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}