syntax_error/
write.rs

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