solar_interface/diagnostics/emitter/
rustc.rs

1//! Annotation collector for displaying diagnostics vendored from Rustc.
2
3use crate::{
4    SourceMap,
5    diagnostics::{Level, MultiSpan, SpanLabel},
6    source_map::{Loc, SourceFile},
7};
8use std::{
9    cmp::{max, min},
10    sync::Arc,
11};
12
13#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
14pub(crate) struct Line {
15    pub(crate) line_index: usize,
16    pub(crate) annotations: Vec<Annotation>,
17}
18
19impl Line {
20    pub(crate) fn set_level(&mut self, level: Level) {
21        for ann in &mut self.annotations {
22            ann.level = Some(level);
23        }
24    }
25}
26
27#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Default)]
28pub(crate) struct AnnotationColumn {
29    /// the (0-indexed) column for *display* purposes, counted in characters, not utf-8 bytes
30    pub(crate) display: usize,
31    /// the (0-indexed) column in the file, counted in characters, not utf-8 bytes.
32    ///
33    /// this may be different from `self.display`,
34    /// e.g. if the file contains hard tabs, because we convert tabs to spaces for error messages.
35    ///
36    /// for example:
37    /// ```text
38    /// (hard tab)hello
39    ///           ^ this is display column 4, but file column 1
40    /// ```
41    ///
42    /// we want to keep around the correct file offset so that column numbers in error messages
43    /// are correct. (motivated by <https://github.com/rust-lang/rust/issues/109537>)
44    pub(crate) file: usize,
45}
46
47impl AnnotationColumn {
48    pub(crate) fn from_loc(loc: &Loc) -> Self {
49        Self { display: loc.col_display, file: loc.col.0 }
50    }
51}
52
53#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
54pub(crate) struct MultilineAnnotation {
55    pub(crate) depth: usize,
56    pub(crate) line_start: usize,
57    pub(crate) line_end: usize,
58    pub(crate) start_col: AnnotationColumn,
59    pub(crate) end_col: AnnotationColumn,
60    pub(crate) is_primary: bool,
61    pub(crate) label: Option<String>,
62    pub(crate) overlaps_exactly: bool,
63}
64
65impl MultilineAnnotation {
66    pub(crate) fn increase_depth(&mut self) {
67        self.depth += 1;
68    }
69
70    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
71    pub(crate) fn same_span(&self, other: &Self) -> bool {
72        self.line_start == other.line_start
73            && self.line_end == other.line_end
74            && self.start_col == other.start_col
75            && self.end_col == other.end_col
76    }
77
78    pub(crate) fn as_start(&self) -> Annotation {
79        Annotation {
80            start_col: self.start_col,
81            end_col: AnnotationColumn {
82                // these might not correspond to the same place anymore,
83                // but that's okay for our purposes
84                display: self.start_col.display + 1,
85                file: self.start_col.file + 1,
86            },
87            is_primary: self.is_primary,
88            label: None,
89            annotation_type: AnnotationType::MultilineStart(self.depth),
90            level: None,
91        }
92    }
93
94    pub(crate) fn as_end(&self) -> Annotation {
95        Annotation {
96            start_col: AnnotationColumn {
97                // these might not correspond to the same place anymore,
98                // but that's okay for our purposes
99                display: self.end_col.display.saturating_sub(1),
100                file: self.end_col.file.saturating_sub(1),
101            },
102            end_col: self.end_col,
103            is_primary: self.is_primary,
104            label: self.label.clone(),
105            annotation_type: AnnotationType::MultilineEnd(self.depth),
106            level: None,
107        }
108    }
109
110    pub(crate) fn as_line(&self) -> Annotation {
111        Annotation {
112            start_col: Default::default(),
113            end_col: Default::default(),
114            is_primary: self.is_primary,
115            label: None,
116            annotation_type: AnnotationType::MultilineLine(self.depth),
117            level: None,
118        }
119    }
120}
121
122#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
123pub(crate) enum AnnotationType {
124    /// Annotation under a single line of code
125    Singleline,
126
127    // The Multiline type above is replaced with the following three in order
128    // to reuse the current label drawing code.
129    //
130    // Each of these corresponds to one part of the following diagram:
131    //
132    //     x |   foo(1 + bar(x,
133    //       |  _________^              < MultilineStart
134    //     x | |             y),        < MultilineLine
135    //       | |______________^ label   < MultilineEnd
136    //     x |       z);
137    /// Annotation marking the first character of a fully shown multiline span
138    MultilineStart(usize),
139    /// Annotation marking the last character of a fully shown multiline span
140    MultilineEnd(usize),
141    /// Line at the left enclosing the lines of a fully shown multiline span
142    // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
143    // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
144    // `draw_multiline_line`.
145    MultilineLine(usize),
146}
147
148#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
149pub(crate) struct Annotation {
150    /// Start column.
151    /// Note that it is important that this field goes
152    /// first, so that when we sort, we sort orderings by start
153    /// column.
154    pub(crate) start_col: AnnotationColumn,
155
156    /// End column within the line (exclusive)
157    pub(crate) end_col: AnnotationColumn,
158
159    /// Is this annotation derived from primary span
160    pub(crate) is_primary: bool,
161
162    /// Optional label to display adjacent to the annotation.
163    pub(crate) label: Option<String>,
164
165    /// Is this a single line, multiline or multiline span minimized down to a
166    /// smaller span.
167    pub(crate) annotation_type: AnnotationType,
168
169    pub(crate) level: Option<Level>,
170}
171
172#[derive(Debug)]
173pub(crate) struct FileWithAnnotatedLines {
174    pub(crate) file: Arc<SourceFile>,
175    pub(crate) lines: Vec<Line>,
176    multiline_depth: usize,
177}
178
179impl FileWithAnnotatedLines {
180    /// Preprocess all the annotations so that they are grouped by file and by line number
181    /// This helps us quickly iterate over the whole message (including secondary file spans)
182    pub(crate) fn collect_annotations(sm: &SourceMap, msp: &MultiSpan) -> Vec<Self> {
183        fn add_annotation_to_file(
184            file_vec: &mut Vec<FileWithAnnotatedLines>,
185            file: Arc<SourceFile>,
186            line_index: usize,
187            ann: Annotation,
188        ) {
189            for slot in file_vec.iter_mut() {
190                // Look through each of our files for the one we're adding to
191                if slot.file.name == file.name {
192                    // See if we already have a line for it
193                    for line_slot in &mut slot.lines {
194                        if line_slot.line_index == line_index {
195                            line_slot.annotations.push(ann);
196                            return;
197                        }
198                    }
199                    // We don't have a line yet, create one
200                    slot.lines.push(Line { line_index, annotations: vec![ann] });
201                    slot.lines.sort();
202                    return;
203                }
204            }
205            // This is the first time we're seeing the file
206            file_vec.push(FileWithAnnotatedLines {
207                file,
208                lines: vec![Line { line_index, annotations: vec![ann] }],
209                multiline_depth: 0,
210            });
211        }
212
213        let mut output = vec![];
214        let mut multiline_annotations = vec![];
215
216        for SpanLabel { span, is_primary, label } in msp.span_labels() {
217            // If we don't have a useful span, pick the primary span if that exists.
218            // Worst case we'll just print an error at the top of the main file.
219            let span = match (span.is_dummy(), msp.primary_span()) {
220                (_, None) | (false, _) => span,
221                (true, Some(span)) => span,
222            };
223
224            let lo = sm.lookup_char_pos(span.lo());
225            let mut hi = sm.lookup_char_pos(span.hi());
226
227            // Watch out for "empty spans". If we get a span like 6..6, we
228            // want to just display a `^` at 6, so convert that to
229            // 6..7. This is degenerate input, but it's best to degrade
230            // gracefully -- and the parser likes to supply a span like
231            // that for EOF, in particular.
232
233            if lo.col_display == hi.col_display && lo.line == hi.line {
234                hi.col_display += 1;
235            }
236
237            let label = label.as_ref().map(|m| m.as_str().to_string());
238
239            if lo.line != hi.line {
240                let ml = MultilineAnnotation {
241                    depth: 1,
242                    line_start: lo.line,
243                    line_end: hi.line,
244                    start_col: AnnotationColumn::from_loc(&lo),
245                    end_col: AnnotationColumn::from_loc(&hi),
246                    is_primary,
247                    label,
248                    overlaps_exactly: false,
249                };
250                multiline_annotations.push((lo.file, ml));
251            } else {
252                let ann = Annotation {
253                    start_col: AnnotationColumn::from_loc(&lo),
254                    end_col: AnnotationColumn::from_loc(&hi),
255                    is_primary,
256                    label,
257                    annotation_type: AnnotationType::Singleline,
258                    level: None,
259                };
260                add_annotation_to_file(&mut output, lo.file, lo.line, ann);
261            };
262        }
263
264        // Find overlapping multiline annotations, put them at different depths
265        multiline_annotations.sort_by_key(|(_, ml)| (ml.line_start, usize::MAX - ml.line_end));
266        for (_, ann) in multiline_annotations.clone() {
267            for (_, a) in multiline_annotations.iter_mut() {
268                // Move all other multiline annotations overlapping with this one
269                // one level to the right.
270                if !(ann.same_span(a))
271                    && num_overlap(ann.line_start, ann.line_end, a.line_start, a.line_end, true)
272                {
273                    a.increase_depth();
274                } else if ann.same_span(a) && &ann != a {
275                    a.overlaps_exactly = true;
276                } else {
277                    break;
278                }
279            }
280        }
281
282        let mut max_depth = 0; // max overlapping multiline spans
283        for (_, ann) in &multiline_annotations {
284            max_depth = max(max_depth, ann.depth);
285        }
286        // Change order of multispan depth to minimize the number of overlaps in the ASCII art.
287        for (_, a) in multiline_annotations.iter_mut() {
288            a.depth = max_depth - a.depth + 1;
289        }
290        for (file, ann) in multiline_annotations {
291            let mut end_ann = ann.as_end();
292            if !ann.overlaps_exactly {
293                // avoid output like
294                //
295                //  |        foo(
296                //  |   _____^
297                //  |  |_____|
298                //  | ||         bar,
299                //  | ||     );
300                //  | ||      ^
301                //  | ||______|
302                //  |  |______foo
303                //  |         baz
304                //
305                // and instead get
306                //
307                //  |       foo(
308                //  |  _____^
309                //  | |         bar,
310                //  | |     );
311                //  | |      ^
312                //  | |      |
313                //  | |______foo
314                //  |        baz
315                add_annotation_to_file(&mut output, file.clone(), ann.line_start, ann.as_start());
316                // 4 is the minimum vertical length of a multiline span when presented: two lines
317                // of code and two lines of underline. This is not true for the special case where
318                // the beginning doesn't have an underline, but the current logic seems to be
319                // working correctly.
320                let middle = min(ann.line_start + 4, ann.line_end);
321                for line in ann.line_start + 1..middle {
322                    // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`).
323                    add_annotation_to_file(&mut output, file.clone(), line, ann.as_line());
324                }
325                let line_end = ann.line_end - 1;
326                if middle < line_end {
327                    add_annotation_to_file(&mut output, file.clone(), line_end, ann.as_line());
328                }
329            } else {
330                end_ann.annotation_type = AnnotationType::Singleline;
331            }
332            add_annotation_to_file(&mut output, file, ann.line_end, end_ann);
333        }
334        for file_vec in output.iter_mut() {
335            file_vec.multiline_depth = max_depth;
336        }
337        output
338    }
339
340    pub(crate) fn set_level(&mut self, level: Level) {
341        for line in &mut self.lines {
342            line.set_level(level);
343        }
344    }
345
346    pub(crate) fn add_lines(&mut self, lines: impl IntoIterator<Item = Line>) {
347        debug_assert!(self.lines.is_sorted(), "file lines should be sorted");
348        for line in lines {
349            match self.lines.binary_search_by_key(&line.line_index, |l| l.line_index) {
350                Ok(i) => {
351                    self.lines[i].annotations.extend(line.annotations);
352                    self.lines[i].annotations.sort();
353                }
354                Err(i) => {
355                    self.lines.insert(i, line);
356                }
357            }
358        }
359        debug_assert!(self.lines.is_sorted(), "file lines should still be sorted");
360    }
361}
362
363fn num_overlap(
364    a_start: usize,
365    a_end: usize,
366    b_start: usize,
367    b_end: usize,
368    inclusive: bool,
369) -> bool {
370    let extra = if inclusive { 1 } else { 0 };
371    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
372}