solar_interface/diagnostics/emitter/
rustc.rs

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