diagnostic/
write.rs

1use crate::SourceID;
2use source_cache::{SourceCache, SourceText};
3use std::ops::Range;
4
5use super::{
6    draw::{StreamAwareFmt, StreamType},
7    Diagnostic, Label, LabelAttach, Show, Write,
8};
9
10// A WARNING, FOR ALL YE WHO VENTURE IN HERE
11//
12// - This code is complex and has a lot of implicit invariants
13// - Yes, it has some bugs
14// - Yes, it needs rewriting
15// - No, you are not expected to understand it. I will probably not understand it either in a month, but that will only
16//   give me a reason to rewrite it
17
18enum LabelKind {
19    Inline,
20    Multiline,
21}
22
23struct LabelInfo<'a> {
24    kind: LabelKind,
25    label: &'a Label,
26}
27
28struct SourceGroup<'a> {
29    id: &'a SourceID,
30    span: Range<u32>,
31    labels: Vec<LabelInfo<'a>>,
32}
33
34impl Diagnostic {
35    fn get_source_groups(&self, cache: &SourceCache) -> Vec<SourceGroup> {
36        let mut groups = Vec::new();
37        for label in self.labels.iter() {
38            let src = match cache.fetch(&label.span.file) {
39                Ok(src) => src,
40                Err(e) => {
41                    let src_display = cache.source_path(&label.span.file);
42                    eprintln!("Unable to fetch identifier '{}': {:?}", Show(src_display), e);
43                    continue;
44                }
45            };
46
47            assert!(label.span.start <= label.span.end, "Label start is after its end");
48
49            let start_line = src.get_offset_line(label.span.start).map(|(_, l, _)| l);
50            let end_line = src.get_offset_line(label.span.end.saturating_sub(1).max(label.span.start)).map(|(_, l, _)| l);
51
52            let label_info =
53                LabelInfo { kind: if start_line == end_line { LabelKind::Inline } else { LabelKind::Multiline }, label };
54
55            if let Some(group) = groups.iter_mut().find(|g: &&mut SourceGroup| g.id == &label.span.file) {
56                group.span.start = group.span.start.min(label.span.start);
57                group.span.end = group.span.end.max(label.span.end);
58                group.labels.push(label_info);
59            }
60            else {
61                groups.push(SourceGroup {
62                    id: &label.span.file,
63                    span: Range { start: label.span.start, end: label.span.end },
64                    labels: vec![label_info],
65                });
66            }
67        }
68        groups
69    }
70
71    /// Write this diagnostic to an implementor of [`Write`].
72    ///
73    /// If using the `concolor` feature, this method assumes that the output is ultimately going to be printed to
74    /// `stderr`.  If you are printing to `stdout`, use the [`write_for_stdout`](Self::write_for_stdout) method instead.
75    ///
76    /// If you wish to write to `stderr` or `stdout`, you can do so via [`Diagnostic::eprint`] or [`Diagnostic::print`] respectively.
77    pub fn write<W: Write>(&self, cache: &SourceCache, w: W) -> std::io::Result<()> {
78        self.write_for_stream(cache, w, StreamType::Stderr)
79    }
80
81    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
82    /// to `stdout`.
83    pub fn write_for_stdout<W: Write>(&self, cache: &SourceCache, w: W) -> std::io::Result<()> {
84        self.write_for_stream(cache, w, StreamType::Stdout)
85    }
86
87    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
88    /// to the given output stream (`stdout` or `stderr`).
89    fn write_for_stream<W: Write>(&self, cache: &SourceCache, mut w: W, s: StreamType) -> std::io::Result<()> {
90        let draw = self.config.characters;
91
92        // --- Header ---
93        let kind_color = self.kind.get_color();
94        let head = match &self.code {
95            Some(s) => format!("{:?}[{:04}]:", self.kind, s),
96            None => format!("{:?}:", self.kind),
97        };
98        write!(w, "{}", head.fg(kind_color, s))?;
99        if self.message.is_empty() {
100            writeln!(w)?;
101        }
102        else {
103            writeln!(w, " {}", self.message)?;
104        }
105        let groups = self.get_source_groups(&cache);
106
107        // Line number maximum width
108        let line_no_width = groups
109            .iter()
110            .filter_map(|SourceGroup { span, id: src_id, .. }| {
111                let src_name = cache.source_path(src_id).map(|d| d.to_string()).unwrap_or_else(|| "<unknown>".to_string());
112
113                let src = match cache.fetch(src_id) {
114                    Ok(src) => src,
115                    Err(e) => {
116                        eprintln!("Unable to fetch identifier {}: {:?}", src_name, e);
117                        return None;
118                    }
119                };
120
121                let line_range = src.get_line_range(span);
122                Some((1..).map(|x| 10u32.pow(x)).take_while(|x| line_range.end as u32 / x != 0).count() + 1)
123            })
124            .max()
125            .unwrap_or(0);
126
127        // --- Source sections ---
128        let groups_len = groups.len();
129        for (group_idx, SourceGroup { id: src_id, span, labels }) in groups.into_iter().enumerate() {
130            let src_name = cache.source_path(src_id).map(|d| d.to_string()).unwrap_or_else(|| "<unknown>".to_string());
131
132            let src = match cache.fetch(src_id) {
133                Ok(src) => src,
134                Err(e) => {
135                    eprintln!("Unable to fetch identifier {}: {:?}", src_name, e);
136                    continue;
137                }
138            };
139
140            let line_range = src.get_line_range(&span);
141            let line_ref = self.get_line_column(src_id, &labels, src);
142            // File name & reference
143            writeln!(
144                w,
145                "{}{}{}{}{}{}{}",
146                Show((' ', line_no_width + 2)),
147                if group_idx == 0 { draw.ltop } else { draw.lcross }.fg(self.config.margin_color(), s),
148                draw.hbar.fg(self.config.margin_color(), s),
149                draw.lbox.fg(self.config.margin_color(), s),
150                src_name,
151                line_ref,
152                draw.rbox.fg(self.config.margin_color(), s),
153            )?;
154
155            if !self.config.compact {
156                writeln!(w, "{}{}", Show((' ', line_no_width + 2)), draw.vbar.fg(self.config.margin_color(), s))?;
157            }
158
159            struct LineLabel<'a> {
160                column: u32,
161                label: &'a Label,
162                multi: bool,
163                draw_msg: bool,
164            }
165
166            // Generate a list of multi-line labels
167            let mut multi_labels = Vec::new();
168            for label_info in &labels {
169                if matches!(label_info.kind, LabelKind::Multiline) {
170                    multi_labels.push(&label_info.label);
171                }
172            }
173
174            // Sort multiline labels by length
175            multi_labels.sort_by_key(|m| -(m.span.length() as isize));
176
177            let write_margin = |w: &mut W,
178                                idx: usize,
179                                is_line: bool,
180                                is_ellipsis: bool,
181                                draw_labels: bool,
182                                report_row: Option<(usize, bool)>,
183                                line_labels: &[LineLabel],
184                                margin_label: &Option<LineLabel>|
185             -> std::io::Result<()> {
186                let line_no_margin = if is_line && !is_ellipsis {
187                    let line_no = format!("{}", idx + 1);
188                    format!("{}{} {}", Show((' ', line_no_width - line_no.chars().count())), line_no, draw.vbar,)
189                        .fg(self.config.margin_color(), s)
190                }
191                else {
192                    format!("{}{}", Show((' ', line_no_width + 1)), if is_ellipsis { draw.vbar_gap } else { draw.vbar })
193                        .fg(self.config.skipped_margin_color(), s)
194                };
195
196                write!(w, " {}{}", line_no_margin, Show(Some(' ').filter(|_| !self.config.compact)),)?;
197
198                // Multi-line margins
199                if draw_labels {
200                    for col in 0..multi_labels.len() + (multi_labels.len() > 0) as usize {
201                        let mut corner = None;
202                        let mut hbar = None;
203                        let mut vbar: Option<&&Label> = None;
204                        let mut margin_ptr = None;
205
206                        let multi_label = multi_labels.get(col);
207                        let line_span = src.get_line(idx).unwrap().range();
208
209                        for (i, label) in multi_labels[0..(col + 1).min(multi_labels.len())].iter().enumerate() {
210                            let margin = margin_label.as_ref().filter(|m| **label as *const _ == m.label as *const _);
211
212                            if label.span.start <= line_span.end && label.span.end > line_span.start {
213                                let is_parent = i != col;
214                                let is_start = line_span.contains(&label.span.start);
215                                let is_end = line_span.contains(&label.last_offset());
216
217                                if let Some(margin) = margin.filter(|_| is_line) {
218                                    margin_ptr = Some((margin, is_start));
219                                }
220                                else if !is_start && (!is_end || is_line) {
221                                    vbar = vbar.or(Some(*label).filter(|_| !is_parent));
222                                }
223                                else if let Some((report_row, is_arrow)) = report_row {
224                                    let label_row = line_labels
225                                        .iter()
226                                        .enumerate()
227                                        .find(|(_, l)| **label as *const _ == l.label as *const _)
228                                        .map_or(0, |(r, _)| r);
229                                    if report_row == label_row {
230                                        if let Some(margin) = margin {
231                                            vbar = Some(&margin.label).filter(|_| col == i);
232                                            if is_start {
233                                                continue;
234                                            }
235                                        }
236
237                                        if is_arrow {
238                                            hbar = Some(**label);
239                                            if !is_parent {
240                                                corner = Some((label, is_start));
241                                            }
242                                        }
243                                        else if !is_start {
244                                            vbar = vbar.or(Some(*label).filter(|_| !is_parent));
245                                        }
246                                    }
247                                    else {
248                                        vbar = vbar
249                                            .or(Some(*label).filter(|_| !is_parent && (is_start ^ (report_row < label_row))));
250                                    }
251                                }
252                            }
253                        }
254
255                        if let (Some((margin, _is_start)), true) = (margin_ptr, is_line) {
256                            let is_col = multi_label.map_or(false, |ml| **ml as *const _ == margin.label as *const _);
257                            let is_limit = col + 1 == multi_labels.len();
258                            if !is_col && !is_limit {
259                                hbar = hbar.or(Some(margin.label));
260                            }
261                        }
262
263                        hbar = hbar.filter(|l| {
264                            margin_label.as_ref().map_or(true, |margin| margin.label as *const _ != *l as *const _) || !is_line
265                        });
266
267                        let (a, b) = if let Some((label, is_start)) = corner {
268                            (if is_start { draw.ltop } else { draw.lbot }.fg(label.color, s), draw.hbar.fg(label.color, s))
269                        }
270                        else if let Some(label) = hbar.filter(|_| vbar.is_some() && !self.config.cross_gap) {
271                            (draw.xbar.fg(label.color, s), draw.hbar.fg(label.color, s))
272                        }
273                        else if let Some(label) = hbar {
274                            (draw.hbar.fg(label.color, s), draw.hbar.fg(label.color, s))
275                        }
276                        else if let Some(label) = vbar {
277                            (if is_ellipsis { draw.vbar_gap } else { draw.vbar }.fg(label.color, s), ' '.fg(None, s))
278                        }
279                        else if let (Some((margin, is_start)), true) = (margin_ptr, is_line) {
280                            let is_col = multi_label.map_or(false, |ml| **ml as *const _ == margin.label as *const _);
281                            let is_limit = col == multi_labels.len();
282                            (
283                                if is_limit {
284                                    draw.rarrow
285                                }
286                                else if is_col {
287                                    if is_start { draw.ltop } else { draw.lcross }
288                                }
289                                else {
290                                    draw.hbar
291                                }
292                                .fg(margin.label.color, s),
293                                if !is_limit { draw.hbar } else { ' ' }.fg(margin.label.color, s),
294                            )
295                        }
296                        else {
297                            (' '.fg(None, s), ' '.fg(None, s))
298                        };
299                        write!(w, "{}", a)?;
300                        if !self.config.compact {
301                            write!(w, "{}", b)?;
302                        }
303                    }
304                }
305
306                Ok(())
307            };
308
309            let mut is_ellipsis = false;
310            for idx in line_range {
311                let line = if let Some(line) = src.get_line(idx) {
312                    line
313                }
314                else {
315                    continue;
316                };
317
318                let margin_label = multi_labels
319                    .iter()
320                    .enumerate()
321                    .filter_map(|(_i, label)| {
322                        let is_start = line.range().contains(&label.span.start);
323                        let is_end = line.range().contains(&label.last_offset());
324                        if is_start {
325                            // TODO: Check to see whether multi is the first on the start line or first on the end line
326                            Some(LineLabel {
327                                column: label.span.start - line.offset,
328                                label: **label,
329                                multi: true,
330                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
331                            })
332                        }
333                        else if is_end {
334                            Some(LineLabel {
335                                column: label.last_offset() - line.offset,
336                                label: **label,
337                                multi: true,
338                                draw_msg: true, // Multi-line spans have their messages drawn at the end
339                            })
340                        }
341                        else {
342                            None
343                        }
344                    })
345                    .min_by_key(|ll| (ll.column, !ll.label.span.start));
346
347                // Generate a list of labels for this line, along with their label columns
348                let mut line_labels = multi_labels
349                    .iter()
350                    .enumerate()
351                    .filter_map(|(_i, label)| {
352                        let is_start = line.range().contains(&label.span.start);
353                        let is_end = line.range().contains(&label.last_offset());
354                        if is_start && margin_label.as_ref().map_or(true, |m| **label as *const _ != m.label as *const _) {
355                            // TODO: Check to see whether multi is the first on the start line or first on the end line
356                            Some(LineLabel {
357                                column: label.span.start - line.offset,
358                                label: **label,
359                                multi: true,
360                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
361                            })
362                        }
363                        else if is_end {
364                            Some(LineLabel {
365                                column: label.last_offset() - line.offset,
366                                label: **label,
367                                multi: true,
368                                draw_msg: true, // Multi-line spans have their messages drawn at the end
369                            })
370                        }
371                        else {
372                            None
373                        }
374                    })
375                    .collect::<Vec<_>>();
376
377                for label_info in
378                    labels.iter().filter(|l| l.label.span.start >= line.range().start && l.label.span.end <= line.range().end)
379                {
380                    if matches!(label_info.kind, LabelKind::Inline) {
381                        line_labels.push(LineLabel {
382                            column: match &self.config.label_attach {
383                                LabelAttach::Start => label_info.label.span.start,
384                                LabelAttach::Middle => (label_info.label.span.start + label_info.label.span.end) / 2,
385                                LabelAttach::End => label_info.label.last_offset(),
386                            }
387                            .max(label_info.label.span.start)
388                                - line.offset,
389                            label: label_info.label,
390                            multi: false,
391                            draw_msg: true,
392                        });
393                    }
394                }
395
396                // Skip this line if we don't have labels for it
397                if line_labels.len() == 0 && margin_label.is_none() {
398                    let within_label = multi_labels.iter().any(|label| label.span.contains(line.range().start));
399                    if !is_ellipsis && within_label {
400                        is_ellipsis = true;
401                    }
402                    else {
403                        if !self.config.compact && !is_ellipsis {
404                            write_margin(&mut w, idx, false, is_ellipsis, false, None, &[], &None)?;
405                            write!(w, "\n")?;
406                        }
407                        is_ellipsis = true;
408                        continue;
409                    }
410                }
411                else {
412                    is_ellipsis = false;
413                }
414
415                // Sort the labels by their columns
416                line_labels.sort_by_key(|ll| (ll.label.order, ll.column, !ll.label.span.start));
417
418                // Determine label bounds so we know where to put error messages
419                let arrow_end_space = if self.config.compact { 1 } else { 2 };
420                let arrow_len = line_labels
421                    .iter()
422                    .fold(0, |l, ll| if ll.multi { line.length } else { l.max(ll.label.span.end.saturating_sub(line.offset)) })
423                    + arrow_end_space;
424
425                // Should we draw a vertical bar as part of a label arrow on this line?
426                let get_vbar = |col, row| {
427                    line_labels
428                        .iter()
429                        // Only labels with notes get an arrow
430                        .enumerate()
431                        .filter(|(_, ll)| {
432                            ll.label.msg.is_some()
433                                && margin_label.as_ref().map_or(true, |m| ll.label as *const _ != m.label as *const _)
434                        })
435                        .find(|(j, ll)| ll.column == col && ((row <= *j && !ll.multi) || (row <= *j && ll.multi)))
436                        .map(|(_, ll)| ll)
437                };
438
439                let get_highlight = |col: u32| {
440                    margin_label
441                        .iter()
442                        .map(|ll| ll.label)
443                        .chain(multi_labels.iter().map(|l| **l))
444                        .chain(line_labels.iter().map(|l| l.label))
445                        .filter(|l| l.span.contains(line.offset + col))
446                        // Prioritise displaying smaller spans
447                        .min_by_key(|l| (-l.priority, l.span.length()))
448                };
449
450                let get_underline = |col| {
451                    line_labels
452                        .iter()
453                        .filter(|ll| {
454                            self.config.underlines
455                                // Underlines only occur for inline spans (highlighting can occur for all spans)
456                                && !ll.multi
457                                && ll.label.span.contains(line.offset + col)
458                        })
459                        // Prioritise displaying smaller spans
460                        .min_by_key(|ll| (-ll.label.priority, ll.label.span.length()))
461                };
462
463                // Margin
464                write_margin(&mut w, idx, true, is_ellipsis, true, None, &line_labels, &margin_label)?;
465
466                // Line
467                if !is_ellipsis {
468                    for (col, c) in line.chars().enumerate() {
469                        let color = if let Some(highlight) = get_highlight(col as u32) {
470                            highlight.color
471                        }
472                        else {
473                            self.config.unimportant_color()
474                        };
475                        let (c, width) = self.config.char_width(c, col);
476                        if c.is_whitespace() {
477                            for _ in 0..width {
478                                write!(w, "{}", c.fg(color, s))?;
479                            }
480                        }
481                        else {
482                            write!(w, "{}", c.fg(color, s))?;
483                        };
484                    }
485                }
486                write!(w, "\n")?;
487
488                // Arrows
489                for row in 0..line_labels.len() {
490                    let line_label = &line_labels[row];
491
492                    if !self.config.compact {
493                        // Margin alternate
494                        write_margin(&mut w, idx, false, is_ellipsis, true, Some((row, false)), &line_labels, &margin_label)?;
495                        // Lines alternate
496                        let mut chars = line.chars();
497                        for col in 0..arrow_len {
498                            let width = chars.next().map_or(1, |c| self.config.char_width(c, col as usize).1);
499
500                            let vbar = get_vbar(col, row);
501                            let underline = get_underline(col).filter(|_| row == 0);
502                            let [c, tail] = if let Some(vbar_ll) = vbar {
503                                let [c, tail] = if underline.is_some() {
504                                    // TODO: Is this good?
505                                    if vbar_ll.label.span.length() <= 1 || true {
506                                        [draw.underbar, draw.underline]
507                                    }
508                                    else if line.offset + col == vbar_ll.label.span.start {
509                                        [draw.ltop, draw.underbar]
510                                    }
511                                    else if line.offset + col == vbar_ll.label.last_offset() {
512                                        [draw.rtop, draw.underbar]
513                                    }
514                                    else {
515                                        [draw.underbar, draw.underline]
516                                    }
517                                }
518                                else if vbar_ll.multi && row == 0 && self.config.multiline_arrows {
519                                    [draw.uarrow, ' ']
520                                }
521                                else {
522                                    [draw.vbar, ' ']
523                                };
524                                [c.fg(vbar_ll.label.color, s), tail.fg(vbar_ll.label.color, s)]
525                            }
526                            else if let Some(underline_ll) = underline {
527                                [draw.underline.fg(underline_ll.label.color, s); 2]
528                            }
529                            else {
530                                [' '.fg(None, s); 2]
531                            };
532
533                            for i in 0..width {
534                                write!(w, "{}", if i == 0 { c } else { tail })?;
535                            }
536                        }
537                        write!(w, "\n")?;
538                    }
539
540                    // Margin
541                    write_margin(&mut w, idx, false, is_ellipsis, true, Some((row, true)), &line_labels, &margin_label)?;
542                    // Lines
543                    let mut chars = line.chars();
544                    for col in 0..arrow_len {
545                        let width = chars.next().map_or(1, |c| self.config.char_width(c, col as usize).1);
546
547                        let is_hbar = (((col > line_label.column) ^ line_label.multi)
548                            || (line_label.label.msg.is_some() && line_label.draw_msg && col > line_label.column))
549                            && line_label.label.msg.is_some();
550                        let [c, tail] = if col == line_label.column
551                            && line_label.label.msg.is_some()
552                            && margin_label.as_ref().map_or(true, |m| line_label.label as *const _ != m.label as *const _)
553                        {
554                            [
555                                if line_label.multi {
556                                    if line_label.draw_msg { draw.mbot } else { draw.rbot }
557                                }
558                                else {
559                                    draw.lbot
560                                }
561                                .fg(line_label.label.color, s),
562                                draw.hbar.fg(line_label.label.color, s),
563                            ]
564                        }
565                        else if let Some(vbar_ll) =
566                            get_vbar(col, row).filter(|_| (col != line_label.column || line_label.label.msg.is_some()))
567                        {
568                            if !self.config.cross_gap && is_hbar {
569                                [draw.xbar.fg(line_label.label.color, s), ' '.fg(line_label.label.color, s)]
570                            }
571                            else if is_hbar {
572                                [draw.hbar.fg(line_label.label.color, s); 2]
573                            }
574                            else {
575                                [
576                                    if vbar_ll.multi && row == 0 && self.config.compact { draw.uarrow } else { draw.vbar }
577                                        .fg(vbar_ll.label.color, s),
578                                    ' '.fg(line_label.label.color, s),
579                                ]
580                            }
581                        }
582                        else if is_hbar {
583                            [draw.hbar.fg(line_label.label.color, s); 2]
584                        }
585                        else {
586                            [' '.fg(None, s); 2]
587                        };
588
589                        if width > 0 {
590                            write!(w, "{}", c)?;
591                        }
592                        for _ in 1..width {
593                            write!(w, "{}", tail)?;
594                        }
595                    }
596                    if line_label.draw_msg {
597                        write!(w, " {}", Show(line_label.label.msg.as_ref()))?;
598                    }
599                    write!(w, "\n")?;
600                }
601            }
602
603            let is_final_group = group_idx + 1 == groups_len;
604
605            // Help
606            if let (Some(note), true) = (&self.help, is_final_group) {
607                if !self.config.compact {
608                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
609                    write!(w, "\n")?;
610                }
611                write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
612                write!(w, "{}: {}\n", "Help".fg(self.config.note_color(), s), note)?;
613            }
614
615            // Note
616            if let (Some(note), true) = (&self.note, is_final_group) {
617                if !self.config.compact {
618                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
619                    write!(w, "\n")?;
620                }
621                write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
622                write!(w, "{}: {}\n", "Note".fg(self.config.note_color(), s), note)?;
623            }
624
625            // Tail of report
626            if !self.config.compact {
627                if is_final_group {
628                    let final_margin = format!("{}{}", Show((draw.hbar, line_no_width + 2)), draw.rbot);
629                    writeln!(w, "{}", final_margin.fg(self.config.margin_color(), s))?;
630                }
631                else {
632                    writeln!(w, "{}{}", Show((' ', line_no_width + 2)), draw.vbar.fg(self.config.margin_color(), s))?;
633                }
634            }
635        }
636        Ok(())
637    }
638
639    fn get_line_column(&self, src_id: &SourceID, labels: &[LabelInfo], src: &SourceText) -> String {
640        let location = if src_id == &self.file {
641            match self.location {
642                Some(s) => s,
643                None => return String::new(),
644            }
645        }
646        else {
647            labels[0].label.span.start
648        };
649        match src.get_offset_line(location) {
650            None => {
651                format!(":{}!", location)
652            }
653            Some((_, idx, col)) => {
654                format!(":{}:{}", idx + 1, col + 1)
655            }
656        }
657    }
658}
659
660impl Label {
661    fn last_offset(&self) -> u32 {
662        self.span.end.saturating_sub(1).max(self.span.start)
663    }
664}