1 use std::ops::Range;
2 
3 use crate::diagnostic::{Diagnostic, LabelStyle};
4 use crate::files::{Error, Files, Location};
5 use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel};
6 use crate::term::Config;
7 
8 /// Count the number of decimal digits in `n`.
count_digits(mut n: usize) -> usize9 fn count_digits(mut n: usize) -> usize {
10     let mut count = 0;
11     while n != 0 {
12         count += 1;
13         n /= 10; // remove last digit
14     }
15     count
16 }
17 
18 /// Output a richly formatted diagnostic, with source code previews.
19 pub struct RichDiagnostic<'diagnostic, 'config, FileId> {
20     diagnostic: &'diagnostic Diagnostic<FileId>,
21     config: &'config Config,
22 }
23 
24 impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId>
25 where
26     FileId: Copy + PartialEq,
27 {
new( diagnostic: &'diagnostic Diagnostic<FileId>, config: &'config Config, ) -> RichDiagnostic<'diagnostic, 'config, FileId>28     pub fn new(
29         diagnostic: &'diagnostic Diagnostic<FileId>,
30         config: &'config Config,
31     ) -> RichDiagnostic<'diagnostic, 'config, FileId> {
32         RichDiagnostic { diagnostic, config }
33     }
34 
render<'files>( &self, files: &'files impl Files<'files, FileId = FileId>, renderer: &mut Renderer<'_, '_>, ) -> Result<(), Error> where FileId: 'files,35     pub fn render<'files>(
36         &self,
37         files: &'files impl Files<'files, FileId = FileId>,
38         renderer: &mut Renderer<'_, '_>,
39     ) -> Result<(), Error>
40     where
41         FileId: 'files,
42     {
43         use std::collections::BTreeMap;
44 
45         struct LabeledFile<'diagnostic, FileId> {
46             file_id: FileId,
47             start: usize,
48             name: String,
49             location: Location,
50             num_multi_labels: usize,
51             lines: BTreeMap<usize, Line<'diagnostic>>,
52             max_label_style: LabelStyle,
53         }
54 
55         impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> {
56             fn get_or_insert_line(
57                 &mut self,
58                 line_index: usize,
59                 line_range: Range<usize>,
60                 line_number: usize,
61             ) -> &mut Line<'diagnostic> {
62                 self.lines.entry(line_index).or_insert_with(|| Line {
63                     range: line_range,
64                     number: line_number,
65                     single_labels: vec![],
66                     multi_labels: vec![],
67                     // This has to be false by default so we know if it must be rendered by another condition already.
68                     must_render: false,
69                 })
70             }
71         }
72 
73         struct Line<'diagnostic> {
74             number: usize,
75             range: std::ops::Range<usize>,
76             // TODO: How do we reuse these allocations?
77             single_labels: Vec<SingleLabel<'diagnostic>>,
78             multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>,
79             must_render: bool,
80         }
81 
82         // TODO: Make this data structure external, to allow for allocation reuse
83         let mut labeled_files = Vec::<LabeledFile<'_, _>>::new();
84         // Keep track of the outer padding to use when rendering the
85         // snippets of source code.
86         let mut outer_padding = 0;
87 
88         // Group labels by file
89         for label in &self.diagnostic.labels {
90             let start_line_index = files.line_index(label.file_id, label.range.start)?;
91             let start_line_number = files.line_number(label.file_id, start_line_index)?;
92             let start_line_range = files.line_range(label.file_id, start_line_index)?;
93             let end_line_index = files.line_index(label.file_id, label.range.end)?;
94             let end_line_number = files.line_number(label.file_id, end_line_index)?;
95             let end_line_range = files.line_range(label.file_id, end_line_index)?;
96 
97             outer_padding = std::cmp::max(outer_padding, count_digits(start_line_number));
98             outer_padding = std::cmp::max(outer_padding, count_digits(end_line_number));
99 
100             // NOTE: This could be made more efficient by using an associative
101             // data structure like a hashmap or B-tree,  but we use a vector to
102             // preserve the order that unique files appear in the list of labels.
103             let labeled_file = match labeled_files
104                 .iter_mut()
105                 .find(|labeled_file| label.file_id == labeled_file.file_id)
106             {
107                 Some(labeled_file) => {
108                     // another diagnostic also referenced this file
109                     if labeled_file.max_label_style > label.style
110                         || (labeled_file.max_label_style == label.style
111                             && labeled_file.start > label.range.start)
112                     {
113                         // this label has a higher style or has the same style but starts earlier
114                         labeled_file.start = label.range.start;
115                         labeled_file.location = files.location(label.file_id, label.range.start)?;
116                         labeled_file.max_label_style = label.style;
117                     }
118                     labeled_file
119                 }
120                 None => {
121                     // no other diagnostic referenced this file yet
122                     labeled_files.push(LabeledFile {
123                         file_id: label.file_id,
124                         start: label.range.start,
125                         name: files.name(label.file_id)?.to_string(),
126                         location: files.location(label.file_id, label.range.start)?,
127                         num_multi_labels: 0,
128                         lines: BTreeMap::new(),
129                         max_label_style: label.style,
130                     });
131                     // this unwrap should never fail because we just pushed an element
132                     labeled_files
133                         .last_mut()
134                         .expect("just pushed an element that disappeared")
135                 }
136             };
137 
138             if start_line_index == end_line_index {
139                 // Single line
140                 //
141                 // ```text
142                 // 2 │ (+ test "")
143                 //   │         ^^ expected `Int` but found `String`
144                 // ```
145                 let label_start = label.range.start - start_line_range.start;
146                 // Ensure that we print at least one caret, even when we
147                 // have a zero-length source range.
148                 let label_end =
149                     usize::max(label.range.end - start_line_range.start, label_start + 1);
150 
151                 let line = labeled_file.get_or_insert_line(
152                     start_line_index,
153                     start_line_range,
154                     start_line_number,
155                 );
156 
157                 // Ensure that the single line labels are lexicographically
158                 // sorted by the range of source code that they cover.
159                 let index = match line.single_labels.binary_search_by(|(_, range, _)| {
160                     // `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)`
161                     // to piggyback off its lexicographic comparison implementation.
162                     (range.start, range.end).cmp(&(label_start, label_end))
163                 }) {
164                     // If the ranges are the same, order the labels in reverse
165                     // to how they were originally specified in the diagnostic.
166                     // This helps with printing in the renderer.
167                     Ok(index) | Err(index) => index,
168                 };
169 
170                 line.single_labels
171                     .insert(index, (label.style, label_start..label_end, &label.message));
172 
173                 // If this line is not rendered, the SingleLabel is not visible.
174                 line.must_render = true;
175             } else {
176                 // Multiple lines
177                 //
178                 // ```text
179                 // 4 │   fizz₁ num = case (mod num 5) (mod num 3) of
180                 //   │ ╭─────────────^
181                 // 5 │ │     0 0 => "FizzBuzz"
182                 // 6 │ │     0 _ => "Fizz"
183                 // 7 │ │     _ 0 => "Buzz"
184                 // 8 │ │     _ _ => num
185                 //   │ ╰──────────────^ `case` clauses have incompatible types
186                 // ```
187 
188                 let label_index = labeled_file.num_multi_labels;
189                 labeled_file.num_multi_labels += 1;
190 
191                 // First labeled line
192                 let label_start = label.range.start - start_line_range.start;
193 
194                 let start_line = labeled_file.get_or_insert_line(
195                     start_line_index,
196                     start_line_range.clone(),
197                     start_line_number,
198                 );
199 
200                 start_line.multi_labels.push((
201                     label_index,
202                     label.style,
203                     MultiLabel::Top(label_start),
204                 ));
205 
206                 // The first line has to be rendered so the start of the label is visible.
207                 start_line.must_render = true;
208 
209                 // Marked lines
210                 //
211                 // ```text
212                 // 5 │ │     0 0 => "FizzBuzz"
213                 // 6 │ │     0 _ => "Fizz"
214                 // 7 │ │     _ 0 => "Buzz"
215                 // ```
216                 for line_index in (start_line_index + 1)..end_line_index {
217                     let line_range = files.line_range(label.file_id, line_index)?;
218                     let line_number = files.line_number(label.file_id, line_index)?;
219 
220                     outer_padding = std::cmp::max(outer_padding, count_digits(line_number));
221 
222                     let line = labeled_file.get_or_insert_line(line_index, line_range, line_number);
223 
224                     line.multi_labels
225                         .push((label_index, label.style, MultiLabel::Left));
226 
227                     // The line should be rendered to match the configuration of how much context to show.
228                     line.must_render |=
229                         // Is this line part of the context after the start of the label?
230                         line_index - start_line_index <= self.config.start_context_lines
231                         ||
232                         // Is this line part of the context before the end of the label?
233                         end_line_index - line_index <= self.config.end_context_lines;
234                 }
235 
236                 // Last labeled line
237                 //
238                 // ```text
239                 // 8 │ │     _ _ => num
240                 //   │ ╰──────────────^ `case` clauses have incompatible types
241                 // ```
242                 let label_end = label.range.end - end_line_range.start;
243 
244                 let end_line = labeled_file.get_or_insert_line(
245                     end_line_index,
246                     end_line_range,
247                     end_line_number,
248                 );
249 
250                 end_line.multi_labels.push((
251                     label_index,
252                     label.style,
253                     MultiLabel::Bottom(label_end, &label.message),
254                 ));
255 
256                 // The last line has to be rendered so the end of the label is visible.
257                 end_line.must_render = true;
258             }
259         }
260 
261         // Header and message
262         //
263         // ```text
264         // error[E0001]: unexpected type in `+` application
265         // ```
266         renderer.render_header(
267             None,
268             self.diagnostic.severity,
269             self.diagnostic.code.as_deref(),
270             self.diagnostic.message.as_str(),
271         )?;
272 
273         // Source snippets
274         //
275         // ```text
276         //   ┌─ test:2:9
277         //   │
278         // 2 │ (+ test "")
279         //   │         ^^ expected `Int` but found `String`
280         //   │
281         // ```
282         let mut labeled_files = labeled_files.into_iter().peekable();
283         while let Some(labeled_file) = labeled_files.next() {
284             let source = files.source(labeled_file.file_id)?;
285             let source = source.as_ref();
286 
287             // Top left border and locus.
288             //
289             // ```text
290             // ┌─ test:2:9
291             // ```
292             if !labeled_file.lines.is_empty() {
293                 renderer.render_snippet_start(
294                     outer_padding,
295                     &Locus {
296                         name: labeled_file.name,
297                         location: labeled_file.location,
298                     },
299                 )?;
300                 renderer.render_snippet_empty(
301                     outer_padding,
302                     self.diagnostic.severity,
303                     labeled_file.num_multi_labels,
304                     &[],
305                 )?;
306             }
307 
308             let mut lines = labeled_file
309                 .lines
310                 .iter()
311                 .filter(|(_, line)| line.must_render)
312                 .peekable();
313 
314             while let Some((line_index, line)) = lines.next() {
315                 renderer.render_snippet_source(
316                     outer_padding,
317                     line.number,
318                     &source[line.range.clone()],
319                     self.diagnostic.severity,
320                     &line.single_labels,
321                     labeled_file.num_multi_labels,
322                     &line.multi_labels,
323                 )?;
324 
325                 // Check to see if we need to render any intermediate stuff
326                 // before rendering the next line.
327                 if let Some((next_line_index, _)) = lines.peek() {
328                     match next_line_index.checked_sub(*line_index) {
329                         // Consecutive lines
330                         Some(1) => {}
331                         // One line between the current line and the next line
332                         Some(2) => {
333                             // Write a source line
334                             let file_id = labeled_file.file_id;
335 
336                             // This line was not intended to be rendered initially.
337                             // To render the line right, we have to get back the original labels.
338                             let labels = labeled_file
339                                 .lines
340                                 .get(&(line_index + 1))
341                                 .map_or(&[][..], |line| &line.multi_labels[..]);
342 
343                             renderer.render_snippet_source(
344                                 outer_padding,
345                                 files.line_number(file_id, line_index + 1)?,
346                                 &source[files.line_range(file_id, line_index + 1)?],
347                                 self.diagnostic.severity,
348                                 &[],
349                                 labeled_file.num_multi_labels,
350                                 labels,
351                             )?;
352                         }
353                         // More than one line between the current line and the next line.
354                         Some(_) | None => {
355                             // Source break
356                             //
357                             // ```text
358                             // ·
359                             // ```
360                             renderer.render_snippet_break(
361                                 outer_padding,
362                                 self.diagnostic.severity,
363                                 labeled_file.num_multi_labels,
364                                 &line.multi_labels,
365                             )?;
366                         }
367                     }
368                 }
369             }
370 
371             // Check to see if we should render a trailing border after the
372             // final line of the snippet.
373             if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() {
374                 // We don't render a border if we are at the final newline
375                 // without trailing notes, because it would end up looking too
376                 // spaced-out in combination with the final new line.
377             } else {
378                 // Render the trailing snippet border.
379                 renderer.render_snippet_empty(
380                     outer_padding,
381                     self.diagnostic.severity,
382                     labeled_file.num_multi_labels,
383                     &[],
384                 )?;
385             }
386         }
387 
388         // Additional notes
389         //
390         // ```text
391         // = expected type `Int`
392         //      found type `String`
393         // ```
394         for note in &self.diagnostic.notes {
395             renderer.render_snippet_note(outer_padding, note)?;
396         }
397         renderer.render_empty()
398     }
399 }
400 
401 /// Output a short diagnostic, with a line number, severity, and message.
402 pub struct ShortDiagnostic<'diagnostic, FileId> {
403     diagnostic: &'diagnostic Diagnostic<FileId>,
404     show_notes: bool,
405 }
406 
407 impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId>
408 where
409     FileId: Copy + PartialEq,
410 {
new( diagnostic: &'diagnostic Diagnostic<FileId>, show_notes: bool, ) -> ShortDiagnostic<'diagnostic, FileId>411     pub fn new(
412         diagnostic: &'diagnostic Diagnostic<FileId>,
413         show_notes: bool,
414     ) -> ShortDiagnostic<'diagnostic, FileId> {
415         ShortDiagnostic {
416             diagnostic,
417             show_notes,
418         }
419     }
420 
render<'files>( &self, files: &'files impl Files<'files, FileId = FileId>, renderer: &mut Renderer<'_, '_>, ) -> Result<(), Error> where FileId: 'files,421     pub fn render<'files>(
422         &self,
423         files: &'files impl Files<'files, FileId = FileId>,
424         renderer: &mut Renderer<'_, '_>,
425     ) -> Result<(), Error>
426     where
427         FileId: 'files,
428     {
429         // Located headers
430         //
431         // ```text
432         // test:2:9: error[E0001]: unexpected type in `+` application
433         // ```
434         let mut primary_labels_encountered = 0;
435         let labels = self.diagnostic.labels.iter();
436         for label in labels.filter(|label| label.style == LabelStyle::Primary) {
437             primary_labels_encountered += 1;
438 
439             renderer.render_header(
440                 Some(&Locus {
441                     name: files.name(label.file_id)?.to_string(),
442                     location: files.location(label.file_id, label.range.start)?,
443                 }),
444                 self.diagnostic.severity,
445                 self.diagnostic.code.as_deref(),
446                 self.diagnostic.message.as_str(),
447             )?;
448         }
449 
450         // Fallback to printing a non-located header if no primary labels were encountered
451         //
452         // ```text
453         // error[E0002]: Bad config found
454         // ```
455         if primary_labels_encountered == 0 {
456             renderer.render_header(
457                 None,
458                 self.diagnostic.severity,
459                 self.diagnostic.code.as_deref(),
460                 self.diagnostic.message.as_str(),
461             )?;
462         }
463 
464         if self.show_notes {
465             // Additional notes
466             //
467             // ```text
468             // = expected type `Int`
469             //      found type `String`
470             // ```
471             for note in &self.diagnostic.notes {
472                 renderer.render_snippet_note(0, note)?;
473             }
474         }
475 
476         Ok(())
477     }
478 }
479