From a20ee76b56c46a593238ce7ac9b9f70a99c43ff4 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Wed, 20 Apr 2016 14:52:31 -0400 Subject: [PATCH] revamp MultiSpan and introduce new snippet code MultiSpan model is now: - set of primary spans - set of span+label pairs Primary spans render with `^^^`, secondary spans with `---`. Labels are placed next to the `^^^` or `---` marker as appropriate. --- src/libsyntax/codemap.rs | 228 +++----- src/libsyntax/errors/snippet/mod.rs | 821 +++++++++++++++++++++++++++ src/libsyntax/errors/snippet/test.rs | 524 +++++++++++++++++ 3 files changed, 1433 insertions(+), 140 deletions(-) create mode 100644 src/libsyntax/errors/snippet/mod.rs create mode 100644 src/libsyntax/errors/snippet/test.rs diff --git a/src/libsyntax/codemap.rs b/src/libsyntax/codemap.rs index 35aa827782d..228af27f4b1 100644 --- a/src/libsyntax/codemap.rs +++ b/src/libsyntax/codemap.rs @@ -32,8 +32,6 @@ use serialize::{Encodable, Decodable, Encoder, Decoder}; use ast::Name; -use errors::emitter::MAX_HIGHLIGHT_LINES; - // _____________________________________________________________________________ // Pos, BytePos, CharPos // @@ -51,7 +49,7 @@ pub struct BytePos(pub u32); /// A character offset. Because of multibyte utf8 characters, a byte offset /// is not equivalent to a character offset. The CodeMap will convert BytePos /// values to CharPos values as necessary. -#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub struct CharPos(pub usize); // FIXME: Lots of boilerplate in these impls, but so far my attempts to fix @@ -132,13 +130,29 @@ pub struct Span { pub expn_id: ExpnId } -/// Spans are converted to MultiSpans just before error reporting, either automatically, -/// generated by line grouping, or manually constructed. -/// In the latter case care should be taken to ensure that spans are ordered, disjoint, -/// and point into the same FileMap. +/// A collection of spans. Spans have two orthogonal attributes: +/// +/// - they can be *primary spans*. In this case they are the locus of +/// the error, and would be rendered with `^^^`. +/// - they can have a *label*. In this case, the label is written next +/// to the mark in the snippet when we render. #[derive(Clone)] pub struct MultiSpan { - pub spans: Vec + primary_spans: Vec, + span_labels: Vec<(Span, String)>, +} + +#[derive(Clone, Debug)] +pub struct SpanLabel { + /// the span we are going to include in the final snippet + pub span: Span, + + /// is this a primary span? This is the "locus" of the message, + /// and is indicated with a `^^^^` underline, versus `----` + pub is_primary: bool, + + /// what label should we attach to this span (if any)? + pub label: Option, } pub const DUMMY_SP: Span = Span { lo: BytePos(0), hi: BytePos(0), expn_id: NO_EXPANSION }; @@ -276,97 +290,76 @@ pub fn original_sp(cm: &CodeMap, sp: Span, enclosing_sp: Span) -> Span { impl MultiSpan { pub fn new() -> MultiSpan { - MultiSpan { spans: Vec::new() } - } - - pub fn to_span_bounds(&self) -> Span { - assert!(!self.spans.is_empty()); - let Span { lo, expn_id, .. } = *self.spans.first().unwrap(); - let Span { hi, .. } = *self.spans.last().unwrap(); - Span { lo: lo, hi: hi, expn_id: expn_id } - } - - /// Merges or inserts the given span into itself. - pub fn push_merge(&mut self, mut sp: Span) { - let mut idx_merged = None; - - for idx in 0.. { - let cur = match self.spans.get(idx) { - Some(s) => *s, - None => break, - }; - // Try to merge with a contained Span - if let Some(union) = cur.merge(sp) { - self.spans[idx] = union; - sp = union; - idx_merged = Some(idx); - break; - } - // Or insert into the first sorted position - if sp.hi <= cur.lo { - self.spans.insert(idx, sp); - idx_merged = Some(idx); - break; - } - } - if let Some(idx) = idx_merged { - // Merge with spans trailing the insertion/merging position - while (idx + 1) < self.spans.len() { - if let Some(union) = self.spans[idx + 1].merge(sp) { - self.spans[idx] = union; - self.spans.remove(idx + 1); - } else { - break; - } - } - } else { - self.spans.push(sp); + MultiSpan { + primary_spans: vec![], + span_labels: vec![] } } - /// Inserts the given span into itself, for use with `end_highlight_lines`. - pub fn push_trim(&mut self, mut sp: Span) { - let mut prev = mk_sp(BytePos(0), BytePos(0)); + pub fn from_span(primary_span: Span) -> MultiSpan { + MultiSpan { + primary_spans: vec![primary_span], + span_labels: vec![] + } + } - if let Some(first) = self.spans.get_mut(0) { - if first.lo > sp.lo { - // Prevent us here from spanning fewer lines - // because of trimming the start of the span - // (this should not be visible, because this method ought - // to not be used in conjunction with `highlight_lines`) - first.lo = sp.lo; + pub fn from_spans(vec: Vec) -> MultiSpan { + MultiSpan { + primary_spans: vec, + span_labels: vec![] + } + } + + pub fn push_primary_span(&mut self, span: Span) { + self.primary_spans.push(span); + } + + pub fn push_span_label(&mut self, span: Span, label: String) { + self.span_labels.push((span, label)); + } + + /// Selects the first primary span (if any) + pub fn primary_span(&self) -> Option { + self.primary_spans.first().cloned() + } + + /// Returns all primary spans. + pub fn primary_spans(&self) -> &[Span] { + &self.primary_spans + } + + /// Returns the strings to highlight. If we have an explicit set, + /// return those, otherwise just give back an (unlabeled) version + /// of the primary span. + pub fn span_labels(&self) -> Vec { + let is_primary = |span| self.primary_spans.contains(&span); + let mut span_labels = vec![]; + + for &(span, ref label) in &self.span_labels { + span_labels.push(SpanLabel { + span: span, + is_primary: is_primary(span), + label: Some(label.clone()) + }); + } + + for &span in &self.primary_spans { + if !span_labels.iter().any(|sl| sl.span == span) { + span_labels.push(SpanLabel { + span: span, + is_primary: true, + label: None + }); } } - for idx in 0.. { - if let Some(sp_trim) = sp.trim_start(prev) { - // Implies `sp.hi > prev.hi` - let cur = match self.spans.get(idx) { - Some(s) => *s, - None => { - sp = sp_trim; - break; - } - }; - // `cur` may overlap with `sp_trim` - if let Some(cur_trim) = cur.trim_start(sp_trim) { - // Implies `sp.hi < cur.hi` - self.spans.insert(idx, sp_trim); - self.spans[idx + 1] = cur_trim; - return; - } else if sp.hi == cur.hi { - return; - } - prev = cur; - } - } - self.spans.push(sp); + span_labels } } impl From for MultiSpan { fn from(span: Span) -> MultiSpan { - MultiSpan { spans: vec![span] } + MultiSpan::from_span(span) } } @@ -929,6 +922,10 @@ impl CodeMap { } pub fn span_to_string(&self, sp: Span) -> String { + if sp == COMMAND_LINE_SP { + return "".to_string(); + } + if self.files.borrow().is_empty() && sp.source_equal(&DUMMY_SP) { return "no-location".to_string(); } @@ -1099,12 +1096,16 @@ impl CodeMap { } pub fn span_to_lines(&self, sp: Span) -> FileLinesResult { + debug!("span_to_lines(sp={:?})", sp); + if sp.lo > sp.hi { return Err(SpanLinesError::IllFormedSpan(sp)); } let lo = self.lookup_char_pos(sp.lo); + debug!("span_to_lines: lo={:?}", lo); let hi = self.lookup_char_pos(sp.hi); + debug!("span_to_lines: hi={:?}", hi); if lo.file.start_pos != hi.file.start_pos { return Err(SpanLinesError::DistinctSources(DistinctSources { @@ -1184,59 +1185,6 @@ impl CodeMap { } } - /// Groups and sorts spans by lines into `MultiSpan`s, where `push` adds them to their group, - /// specifying the unification behaviour for overlapping spans. - /// Spans overflowing a line are put into their own one-element-group. - pub fn custom_group_spans(&self, mut spans: Vec, push: F) -> Vec - where F: Fn(&mut MultiSpan, Span) - { - spans.sort_by(|a, b| a.lo.cmp(&b.lo)); - let mut groups = Vec::::new(); - let mut overflowing = vec![]; - let mut prev_expn = ExpnId(!2u32); - let mut prev_file = !0usize; - let mut prev_line = !0usize; - let mut err_size = 0; - - for sp in spans { - let line = self.lookup_char_pos(sp.lo).line; - let line_hi = self.lookup_char_pos(sp.hi).line; - if line != line_hi { - overflowing.push(sp.into()); - continue - } - let file = self.lookup_filemap_idx(sp.lo); - - if err_size < MAX_HIGHLIGHT_LINES && sp.expn_id == prev_expn && file == prev_file { - // `push` takes care of sorting, trimming, and merging - push(&mut groups.last_mut().unwrap(), sp); - if line != prev_line { - err_size += 1; - } - } else { - groups.push(sp.into()); - err_size = 1; - } - prev_expn = sp.expn_id; - prev_file = file; - prev_line = line; - } - groups.extend(overflowing); - groups - } - - /// Groups and sorts spans by lines into `MultiSpan`s, merging overlapping spans. - /// Spans overflowing a line are put into their own one-element-group. - pub fn group_spans(&self, spans: Vec) -> Vec { - self.custom_group_spans(spans, |msp, sp| msp.push_merge(sp)) - } - - /// Like `group_spans`, but trims overlapping spans instead of - /// merging them (for use with `end_highlight_lines`) - pub fn end_group_spans(&self, spans: Vec) -> Vec { - self.custom_group_spans(spans, |msp, sp| msp.push_trim(sp)) - } - pub fn get_filemap(&self, filename: &str) -> Rc { for fm in self.files.borrow().iter() { if filename == fm.name { diff --git a/src/libsyntax/errors/snippet/mod.rs b/src/libsyntax/errors/snippet/mod.rs new file mode 100644 index 00000000000..cd8f705ab2e --- /dev/null +++ b/src/libsyntax/errors/snippet/mod.rs @@ -0,0 +1,821 @@ +// Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// Code for annotating snippets. + +use codemap::{CharPos, CodeMap, FileMap, LineInfo, Span}; +use std::cmp; +use std::rc::Rc; +use std::mem; +use std::ops::Range; + +#[cfg(test)] +mod test; + +pub struct SnippetData { + codemap: Rc, + files: Vec, +} + +pub struct FileInfo { + file: Rc, + + /// The "primary file", if any, gets a `-->` marker instead of + /// `>>>`, and has a line-number/column printed and not just a + /// filename. It appears first in the listing. It is known to + /// contain at least one primary span, though primary spans (which + /// are designated with `^^^`) may also occur in other files. + primary_span: Option, + + lines: Vec, +} + +struct Line { + line_index: usize, + annotations: Vec, +} + +#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)] +struct Annotation { + /// Start column, 0-based indexing -- counting *characters*, not + /// utf-8 bytes. Note that it is important that this field goes + /// first, so that when we sort, we sort orderings by start + /// column. + start_col: usize, + + /// End column within the line. + end_col: usize, + + /// Is this annotation derived from primary span + is_primary: bool, + + /// Optional label to display adjacent to the annotation. + label: Option, +} + +#[derive(Debug)] +pub struct RenderedLine { + pub text: Vec, + pub kind: RenderedLineKind, +} + +#[derive(Debug)] +pub struct StyledString { + pub text: String, + pub style: Style, +} + +#[derive(Debug)] +pub struct StyledBuffer { + text: Vec>, + styles: Vec> +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Style { + FileNameStyle, + LineAndColumn, + LineNumber, + Quotation, + UnderlinePrimary, + UnderlineSecondary, + LabelPrimary, + LabelSecondary, + NoStyle, +} +use self::Style::*; + +#[derive(Debug, Clone)] +pub enum RenderedLineKind { + PrimaryFileName, + OtherFileName, + SourceText { + file: Rc, + line_index: usize, + }, + Annotations, + Elision, +} +use self::RenderedLineKind::*; + +impl SnippetData { + pub fn new(codemap: Rc, + primary_span: Option) // (*) + -> Self { + // (*) The primary span indicates the file that must appear + // first, and which will have a line number etc in its + // name. Outside of tests, this is always `Some`, but for many + // tests it's not relevant to test this portion of the logic, + // and it's tedious to pick a primary span (read: tedious to + // port older tests that predate the existence of a primary + // span). + + debug!("SnippetData::new(primary_span={:?})", primary_span); + + let mut data = SnippetData { + codemap: codemap.clone(), + files: vec![] + }; + if let Some(primary_span) = primary_span { + let lo = codemap.lookup_char_pos(primary_span.lo); + data.files.push( + FileInfo { + file: lo.file, + primary_span: Some(primary_span), + lines: vec![], + }); + } + data + } + + pub fn push(&mut self, span: Span, is_primary: bool, label: Option) { + debug!("SnippetData::push(span={:?}, is_primary={}, label={:?})", + span, is_primary, label); + + let file_lines = match self.codemap.span_to_lines(span) { + Ok(file_lines) => file_lines, + Err(_) => { + // ignore unprintable spans completely. + return; + } + }; + + self.file(&file_lines.file) + .push_lines(&file_lines.lines, is_primary, label); + } + + fn file(&mut self, file_map: &Rc) -> &mut FileInfo { + let index = self.files.iter().position(|f| f.file.name == file_map.name); + if let Some(index) = index { + return &mut self.files[index]; + } + + self.files.push( + FileInfo { + file: file_map.clone(), + lines: vec![], + primary_span: None, + }); + self.files.last_mut().unwrap() + } + + pub fn render_lines(&self) -> Vec { + debug!("SnippetData::render_lines()"); + + let mut rendered_lines: Vec<_> = + self.files.iter() + .flat_map(|f| f.render_file_lines(&self.codemap)) + .collect(); + prepend_prefixes(&mut rendered_lines); + trim_lines(&mut rendered_lines); + rendered_lines + } +} + +pub trait StringSource { + fn make_string(self) -> String; +} + +impl StringSource for String { + fn make_string(self) -> String { + self + } +} + +impl StringSource for Vec { + fn make_string(self) -> String { + self.into_iter().collect() + } +} + +impl From<(S, Style, RenderedLineKind)> for RenderedLine + where S: StringSource +{ + fn from((text, style, kind): (S, Style, RenderedLineKind)) -> Self { + RenderedLine { + text: vec![StyledString { + text: text.make_string(), + style: style, + }], + kind: kind, + } + } +} + +impl From<(S1, Style, S2, Style, RenderedLineKind)> for RenderedLine + where S1: StringSource, S2: StringSource +{ + fn from(tuple: (S1, Style, S2, Style, RenderedLineKind)) + -> Self { + let (text1, style1, text2, style2, kind) = tuple; + RenderedLine { + text: vec![ + StyledString { + text: text1.make_string(), + style: style1, + }, + StyledString { + text: text2.make_string(), + style: style2, + } + ], + kind: kind, + } + } +} + +impl RenderedLine { + fn trim_last(&mut self) { + if !self.text.is_empty() { + let last_text = &mut self.text.last_mut().unwrap().text; + let len = last_text.trim_right().len(); + last_text.truncate(len); + } + } +} + +impl RenderedLineKind { + fn prefix(&self) -> StyledString { + match *self { + SourceText { file: _, line_index } => + StyledString { + text: format!("{}", line_index + 1), + style: LineNumber, + }, + Elision => + StyledString { + text: String::from("..."), + style: LineNumber, + }, + PrimaryFileName | + OtherFileName | + Annotations => + StyledString { + text: String::from(""), + style: LineNumber, + }, + } + } +} + +impl StyledBuffer { + fn new() -> StyledBuffer { + StyledBuffer { text: vec![], styles: vec![] } + } + + fn render(&self, source_kind: RenderedLineKind) -> Vec { + let mut output: Vec = vec![]; + let mut styled_vec: Vec = vec![]; + + for (row, row_style) in self.text.iter().zip(&self.styles) { + let mut current_style = NoStyle; + let mut current_text = String::new(); + + for (&c, &s) in row.iter().zip(row_style) { + if s != current_style { + if !current_text.is_empty() { + styled_vec.push(StyledString { text: current_text, style: current_style }); + } + current_style = s; + current_text = String::new(); + } + current_text.push(c); + } + if !current_text.is_empty() { + styled_vec.push(StyledString { text: current_text, style: current_style }); + } + + if output.is_empty() { + //We know our first output line is source and the rest are highlights and labels + output.push(RenderedLine { text: styled_vec, kind: source_kind.clone() }); + } else { + output.push(RenderedLine { text: styled_vec, kind: Annotations }); + } + styled_vec = vec![]; + } + + output + } + + fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) { + while line >= self.text.len() { + self.text.push(vec![]); + self.styles.push(vec![]); + } + + if col < self.text[line].len() { + self.text[line][col] = chr; + self.styles[line][col] = style; + } else { + while self.text[line].len() < col { + self.text[line].push(' '); + self.styles[line].push(NoStyle); + } + self.text[line].push(chr); + self.styles[line].push(style); + } + } + + fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) { + let mut n = col; + for c in string.chars() { + self.putc(line, n, c, style); + n += 1; + } + } + + fn set_style(&mut self, line: usize, col: usize, style: Style) { + if self.styles.len() > line && self.styles[line].len() > col { + self.styles[line][col] = style; + } + } + + fn append(&mut self, line: usize, string: &str, style: Style) { + if line >= self.text.len() { + self.puts(line, 0, string, style); + } else { + let col = self.text[line].len(); + self.puts(line, col, string, style); + } + } +} + +impl FileInfo { + fn push_lines(&mut self, + lines: &[LineInfo], + is_primary: bool, + label: Option) { + assert!(lines.len() > 0); + + // If a span covers multiple lines, just put the label on the + // first one. This is a sort of arbitrary choice and not + // obviously correct. + let (line0, remaining_lines) = lines.split_first().unwrap(); + let index = self.ensure_source_line(line0.line_index); + self.lines[index].push_annotation(line0.start_col, + line0.end_col, + is_primary, + label); + for line in remaining_lines { + if line.end_col > line.start_col { + let index = self.ensure_source_line(line.line_index); + self.lines[index].push_annotation(line.start_col, + line.end_col, + is_primary, + None); + } + } + } + + /// Ensure that we have a `Line` struct corresponding to + /// `line_index` in the file. If we already have some other lines, + /// then this will add the intervening lines to ensure that we + /// have a complete snippet. (Note that when we finally display, + /// some of those lines may be elided.) + fn ensure_source_line(&mut self, line_index: usize) -> usize { + if self.lines.is_empty() { + self.lines.push(Line::new(line_index)); + return 0; + } + + // Find the range of lines we have thus far. + let first_line_index = self.lines.first().unwrap().line_index; + let last_line_index = self.lines.last().unwrap().line_index; + assert!(first_line_index <= last_line_index); + + // If the new line is lower than all the lines we have thus + // far, then insert the new line and any intervening lines at + // the front. In a silly attempt at micro-optimization, we + // don't just call `insert` repeatedly, but instead make a new + // (empty) vector, pushing the new lines onto it, and then + // appending the old vector. + if line_index < first_line_index { + let lines = mem::replace(&mut self.lines, vec![]); + self.lines.extend( + (line_index .. first_line_index) + .map(|line| Line::new(line)) + .chain(lines)); + return 0; + } + + // If the new line comes after the ones we have so far, insert + // lines for it. + if line_index > last_line_index { + self.lines.extend( + (last_line_index+1 .. line_index+1) + .map(|line| Line::new(line))); + return self.lines.len() - 1; + } + + // Otherwise it should already exist. + return line_index - first_line_index; + } + + fn render_file_lines(&self, codemap: &Rc) -> Vec { + // Group our lines by those with annotations and those without + let mut lines_iter = self.lines.iter().peekable(); + + let mut line_groups = vec![]; + + loop { + match lines_iter.next() { + None => break, + Some(line) if line.annotations.is_empty() => { + // Collect unannotated group + let mut unannotated_group : Vec<&Line> = vec![]; + + unannotated_group.push(line); + + loop { + let next_line = + match lines_iter.peek() { + None => break, + Some(x) if !x.annotations.is_empty() => break, + Some(x) => x.clone() + }; + + unannotated_group.push(next_line); + lines_iter.next(); + } + + line_groups.push((false, unannotated_group)); + } + Some(line) => { + // Collect annotated group + let mut annotated_group : Vec<&Line> = vec![]; + + annotated_group.push(line); + + loop { + let next_line = + match lines_iter.peek() { + None => break, + Some(x) if x.annotations.is_empty() => break, + Some(x) => x.clone() + }; + + annotated_group.push(next_line); + lines_iter.next(); + } + + line_groups.push((true, annotated_group)); + } + } + } + + let mut output = vec![]; + + // First insert the name of the file. + match self.primary_span { + Some(span) => { + let lo = codemap.lookup_char_pos(span.lo); + output.push(RenderedLine { + text: vec![StyledString { + text: lo.file.name.clone(), + style: FileNameStyle, + }, StyledString { + text: format!(":{}:{}", lo.line, lo.col.0 + 1), + style: LineAndColumn, + }], + kind: PrimaryFileName, + }); + } + None => { + output.push(RenderedLine { + text: vec![StyledString { + text: self.file.name.clone(), + style: FileNameStyle, + }], + kind: OtherFileName, + }); + } + } + + for &(is_annotated, ref group) in line_groups.iter() { + if is_annotated { + let mut annotation_ends_at_eol = false; + let mut prev_ends_at_eol = false; + let mut elide_unlabeled_region = false; + + for group_line in group.iter() { + let source_string_len = + self.file.get_line(group_line.line_index) + .map(|s| s.len()) + .unwrap_or(0); + + for annotation in &group_line.annotations { + if annotation.end_col == source_string_len { + annotation_ends_at_eol = true; + } + } + + let is_single_unlabeled_annotated_line = + if group_line.annotations.len() == 1 { + if let Some(annotation) = group_line.annotations.first() { + match annotation.label { + Some(_) => false, + None => annotation.start_col == 0 && + annotation.end_col == source_string_len + } + } else { + false + } + } else { + false + }; + + if prev_ends_at_eol && is_single_unlabeled_annotated_line { + if !elide_unlabeled_region { + output.push(RenderedLine::from((String::new(), + NoStyle, Elision))); + elide_unlabeled_region = true; + prev_ends_at_eol = true; + } + continue; + } + + let mut v = self.render_line(group_line); + output.append(&mut v); + + prev_ends_at_eol = annotation_ends_at_eol; + } + } else { + if group.len() > 1 { + output.push(RenderedLine::from((String::new(), NoStyle, Elision))); + } else { + let mut v: Vec = + group.iter().flat_map(|line| self.render_line(line)).collect(); + output.append(&mut v); + } + } + } + + output + } + + fn render_line(&self, line: &Line) -> Vec { + let source_string = self.file.get_line(line.line_index) + .unwrap_or(""); + let source_kind = SourceText { + file: self.file.clone(), + line_index: line.line_index, + }; + + let mut styled_buffer = StyledBuffer::new(); + + // First create the source line we will highlight. + styled_buffer.append(0, &source_string, Quotation); + + if line.annotations.is_empty() { + return styled_buffer.render(source_kind); + } + + // We want to display like this: + // + // vec.push(vec.pop().unwrap()); + // --- ^^^ _ previous borrow ends here + // | | + // | error occurs here + // previous borrow of `vec` occurs here + // + // But there are some weird edge cases to be aware of: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here + // || + // |this makes no sense + // previous borrow of `vec` occurs here + // + // For this reason, we group the lines into "highlight lines" + // and "annotations lines", where the highlight lines have the `~`. + + //let mut highlight_line = Self::whitespace(&source_string); + + // Sort the annotations by (start, end col) + let mut annotations = line.annotations.clone(); + annotations.sort(); + + // Next, create the highlight line. + for annotation in &annotations { + for p in annotation.start_col .. annotation.end_col { + if annotation.is_primary { + styled_buffer.putc(1, p, '^', UnderlinePrimary); + styled_buffer.set_style(0, p, UnderlinePrimary); + } else { + styled_buffer.putc(1, p, '-', UnderlineSecondary); + } + } + } + + // Now we are going to write labels in. To start, we'll exclude + // the annotations with no labels. + let (labeled_annotations, unlabeled_annotations): (Vec<_>, _) = + annotations.into_iter() + .partition(|a| a.label.is_some()); + + // If there are no annotations that need text, we're done. + if labeled_annotations.is_empty() { + return styled_buffer.render(source_kind); + } + + // Now add the text labels. We try, when possible, to stick the rightmost + // annotation at the end of the highlight line: + // + // vec.push(vec.pop().unwrap()); + // --- --- - previous borrow ends here + // + // But sometimes that's not possible because one of the other + // annotations overlaps it. For example, from the test + // `span_overlap_label`, we have the following annotations + // (written on distinct lines for clarity): + // + // fn foo(x: u32) { + // -------------- + // - + // + // In this case, we can't stick the rightmost-most label on + // the highlight line, or we would get: + // + // fn foo(x: u32) { + // -------- x_span + // | + // fn_span + // + // which is totally weird. Instead we want: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // fn_span + // + // which is...less weird, at least. In fact, in general, if + // the rightmost span overlaps with any other span, we should + // use the "hang below" version, so we can at least make it + // clear where the span *starts*. + let mut labeled_annotations = &labeled_annotations[..]; + match labeled_annotations.split_last().unwrap() { + (last, previous) => { + if previous.iter() + .chain(&unlabeled_annotations) + .all(|a| !overlaps(a, last)) + { + // append the label afterwards; we keep it in a separate + // string + let highlight_label: String = format!(" {}", last.label.as_ref().unwrap()); + if last.is_primary { + styled_buffer.append(1, &highlight_label, LabelPrimary); + } else { + styled_buffer.append(1, &highlight_label, LabelSecondary); + } + labeled_annotations = previous; + } + } + } + + // If that's the last annotation, we're done + if labeled_annotations.is_empty() { + return styled_buffer.render(source_kind); + } + + for (index, annotation) in labeled_annotations.iter().enumerate() { + // Leave: + // - 1 extra line + // - One line for each thing that comes after + let comes_after = labeled_annotations.len() - index - 1; + let blank_lines = 3 + comes_after; + + // For each blank line, draw a `|` at our column. The + // text ought to be long enough for this. + for index in 2..blank_lines { + if annotation.is_primary { + styled_buffer.putc(index, annotation.start_col, '|', UnderlinePrimary); + } else { + styled_buffer.putc(index, annotation.start_col, '|', UnderlineSecondary); + } + } + + if annotation.is_primary { + styled_buffer.puts(blank_lines, annotation.start_col, + annotation.label.as_ref().unwrap(), LabelPrimary); + } else { + styled_buffer.puts(blank_lines, annotation.start_col, + annotation.label.as_ref().unwrap(), LabelSecondary); + } + } + + styled_buffer.render(source_kind) + } +} + +fn prepend_prefixes(rendered_lines: &mut [RenderedLine]) { + let prefixes: Vec<_> = + rendered_lines.iter() + .map(|rl| rl.kind.prefix()) + .collect(); + + // find the max amount of spacing we need; add 1 to + // p.text.len() to leave space between the prefix and the + // source text + let padding_len = + prefixes.iter() + .map(|p| if p.text.len() == 0 { 0 } else { p.text.len() + 1 }) + .max() + .unwrap_or(0); + + // Ensure we insert at least one character of padding, so that the + // `-->` arrows can fit etc. + let padding_len = cmp::max(padding_len, 1); + + for (mut prefix, line) in prefixes.into_iter().zip(rendered_lines) { + let extra_spaces = (prefix.text.len() .. padding_len).map(|_| ' '); + prefix.text.extend(extra_spaces); + match line.kind { + RenderedLineKind::Elision => { + line.text.insert(0, prefix); + } + RenderedLineKind::PrimaryFileName => { + // --> filename + // 22 |> + // ^ + // padding_len + let dashes = (0..padding_len - 1).map(|_| ' ') + .chain(Some('-')) + .chain(Some('-')) + .chain(Some('>')) + .chain(Some(' ')); + line.text.insert(0, StyledString {text: dashes.collect(), + style: LineNumber}) + } + RenderedLineKind::OtherFileName => { + // >>>>> filename + // 22 |> + // ^ + // padding_len + let dashes = (0..padding_len + 2).map(|_| '>') + .chain(Some(' ')); + line.text.insert(0, StyledString {text: dashes.collect(), + style: LineNumber}) + } + _ => { + line.text.insert(0, prefix); + line.text.insert(1, StyledString {text: String::from("|> "), + style: LineNumber}) + } + } + } +} + +fn trim_lines(rendered_lines: &mut [RenderedLine]) { + for line in rendered_lines { + while !line.text.is_empty() { + line.trim_last(); + if line.text.last().unwrap().text.is_empty() { + line.text.pop(); + } else { + break; + } + } + } +} + +impl Line { + fn new(line_index: usize) -> Line { + Line { + line_index: line_index, + annotations: vec![] + } + } + + fn push_annotation(&mut self, + start: CharPos, + end: CharPos, + is_primary: bool, + label: Option) { + self.annotations.push(Annotation { + start_col: start.0, + end_col: end.0, + is_primary: is_primary, + label: label, + }); + } +} + +fn overlaps(a1: &Annotation, + a2: &Annotation) + -> bool +{ + between(a1.start_col, a2.start_col .. a2.end_col) || + between(a2.start_col, a1.start_col .. a1.end_col) +} + +fn between(v: usize, range: Range) -> bool { + v >= range.start && v < range.end +} diff --git a/src/libsyntax/errors/snippet/test.rs b/src/libsyntax/errors/snippet/test.rs new file mode 100644 index 00000000000..44ece285b1b --- /dev/null +++ b/src/libsyntax/errors/snippet/test.rs @@ -0,0 +1,524 @@ +// Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// Code for testing annotated snippets. + +use codemap::{BytePos, CodeMap, FileMap, NO_EXPANSION, Span}; +use std::rc::Rc; +use super::{RenderedLine, SnippetData}; + +/// Returns the span corresponding to the `n`th occurrence of +/// `substring` in `source_text`. +trait CodeMapExtension { + fn span_substr(&self, + file: &Rc, + source_text: &str, + substring: &str, + n: usize) + -> Span; +} + +impl CodeMapExtension for CodeMap { + fn span_substr(&self, + file: &Rc, + source_text: &str, + substring: &str, + n: usize) + -> Span + { + println!("span_substr(file={:?}/{:?}, substring={:?}, n={})", + file.name, file.start_pos, substring, n); + let mut i = 0; + let mut hi = 0; + loop { + let offset = source_text[hi..].find(substring).unwrap_or_else(|| { + panic!("source_text `{}` does not have {} occurrences of `{}`, only {}", + source_text, n, substring, i); + }); + let lo = hi + offset; + hi = lo + substring.len(); + if i == n { + let span = Span { + lo: BytePos(lo as u32 + file.start_pos.0), + hi: BytePos(hi as u32 + file.start_pos.0), + expn_id: NO_EXPANSION, + }; + assert_eq!(&self.span_to_snippet(span).unwrap()[..], + substring); + return span; + } + i += 1; + } + } +} + +fn splice(start: Span, end: Span) -> Span { + Span { + lo: start.lo, + hi: end.hi, + expn_id: NO_EXPANSION, + } +} + +fn make_string(lines: &[RenderedLine]) -> String { + lines.iter() + .flat_map(|rl| { + rl.text.iter() + .map(|s| &s.text[..]) + .chain(Some("\n")) + }) + .collect() +} + +#[test] +fn one_line() { + let file_text = r#" +fn foo() { + vec.push(vec.pop().unwrap()); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + let span_vec0 = cm.span_substr(&foo, file_text, "vec", 0); + let span_vec1 = cm.span_substr(&foo, file_text, "vec", 1); + let span_semi = cm.span_substr(&foo, file_text, ";", 0); + + let mut snippet = SnippetData::new(cm, None); + snippet.push(span_vec0, false, Some(format!("previous borrow of `vec` occurs here"))); + snippet.push(span_vec1, false, Some(format!("error occurs here"))); + snippet.push(span_semi, false, Some(format!("previous borrow ends here"))); + + let lines = snippet.render_lines(); + println!("{:#?}", lines); + + let text: String = make_string(&lines); + + println!("text=\n{}", text); + assert_eq!(&text[..], &r#" +>>>> foo.rs +3 |> vec.push(vec.pop().unwrap()); + |> --- --- - previous borrow ends here + |> | | + |> | error occurs here + |> previous borrow of `vec` occurs here +"#[1..]); +} + +#[test] +fn two_files() { + let file_text_foo = r#" +fn foo() { + vec.push(vec.pop().unwrap()); +} +"#; + + let file_text_bar = r#" +fn bar() { + // these blank links here + // serve to ensure that the line numbers + // from bar.rs + // require more digits + + + + + + + + + + + vec.push(); + + // this line will get elided + + vec.pop().unwrap()); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo_map = cm.new_filemap_and_lines("foo.rs", file_text_foo); + let span_foo_vec0 = cm.span_substr(&foo_map, file_text_foo, "vec", 0); + let span_foo_vec1 = cm.span_substr(&foo_map, file_text_foo, "vec", 1); + let span_foo_semi = cm.span_substr(&foo_map, file_text_foo, ";", 0); + + let bar_map = cm.new_filemap_and_lines("bar.rs", file_text_bar); + let span_bar_vec0 = cm.span_substr(&bar_map, file_text_bar, "vec", 0); + let span_bar_vec1 = cm.span_substr(&bar_map, file_text_bar, "vec", 1); + let span_bar_semi = cm.span_substr(&bar_map, file_text_bar, ";", 0); + + let mut snippet = SnippetData::new(cm, Some(span_foo_vec1)); + snippet.push(span_foo_vec0, false, Some(format!("a"))); + snippet.push(span_foo_vec1, true, Some(format!("b"))); + snippet.push(span_foo_semi, false, Some(format!("c"))); + snippet.push(span_bar_vec0, false, Some(format!("d"))); + snippet.push(span_bar_vec1, false, Some(format!("e"))); + snippet.push(span_bar_semi, false, Some(format!("f"))); + + let lines = snippet.render_lines(); + println!("{:#?}", lines); + + let text: String = make_string(&lines); + + println!("text=\n{}", text); + + // Note that the `|>` remain aligned across both files: + assert_eq!(&text[..], &r#" + --> foo.rs:3:14 +3 |> vec.push(vec.pop().unwrap()); + |> --- ^^^ - c + |> | | + |> | b + |> a +>>>>>> bar.rs +17 |> vec.push(); + |> --- - f + |> | + |> d +... +21 |> vec.pop().unwrap()); + |> --- e +"#[1..]); +} + +#[test] +fn multi_line() { + let file_text = r#" +fn foo() { + let name = find_id(&data, 22).unwrap(); + + // Add one more item we forgot to the vector. Silly us. + data.push(Data { name: format!("Hera"), id: 66 }); + + // Print everything out. + println!("Name: {:?}", name); + println!("Data: {:?}", data); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + let span_data0 = cm.span_substr(&foo, file_text, "data", 0); + let span_data1 = cm.span_substr(&foo, file_text, "data", 1); + let span_rbrace = cm.span_substr(&foo, file_text, "}", 3); + + let mut snippet = SnippetData::new(cm, None); + snippet.push(span_data0, false, Some(format!("immutable borrow begins here"))); + snippet.push(span_data1, false, Some(format!("mutable borrow occurs here"))); + snippet.push(span_rbrace, false, Some(format!("immutable borrow ends here"))); + + let lines = snippet.render_lines(); + println!("{:#?}", lines); + + let text: String = make_string(&lines); + + println!("text=\n{}", text); + assert_eq!(&text[..], &r#" +>>>>>> foo.rs +3 |> let name = find_id(&data, 22).unwrap(); + |> ---- immutable borrow begins here +... +6 |> data.push(Data { name: format!("Hera"), id: 66 }); + |> ---- mutable borrow occurs here +... +11 |> } + |> - immutable borrow ends here +"#[1..]); +} + +#[test] +fn overlapping() { + let file_text = r#" +fn foo() { + vec.push(vec.pop().unwrap()); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + let span0 = cm.span_substr(&foo, file_text, "vec.push", 0); + let span1 = cm.span_substr(&foo, file_text, "vec", 0); + let span2 = cm.span_substr(&foo, file_text, "ec.push", 0); + let span3 = cm.span_substr(&foo, file_text, "unwrap", 0); + + let mut snippet = SnippetData::new(cm, None); + snippet.push(span0, false, Some(format!("A"))); + snippet.push(span1, false, Some(format!("B"))); + snippet.push(span2, false, Some(format!("C"))); + snippet.push(span3, false, Some(format!("D"))); + + let lines = snippet.render_lines(); + println!("{:#?}", lines); + let text: String = make_string(&lines); + + println!("text=r#\"\n{}\".trim_left()", text); + assert_eq!(&text[..], &r#" +>>>> foo.rs +3 |> vec.push(vec.pop().unwrap()); + |> -------- ------ D + |> || + |> |C + |> A + |> B +"#[1..]); +} + +#[test] +fn one_line_out_of_order() { + let file_text = r#" +fn foo() { + vec.push(vec.pop().unwrap()); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + let span_vec0 = cm.span_substr(&foo, file_text, "vec", 0); + let span_vec1 = cm.span_substr(&foo, file_text, "vec", 1); + let span_semi = cm.span_substr(&foo, file_text, ";", 0); + + // intentionally don't push the snippets left to right + let mut snippet = SnippetData::new(cm, None); + snippet.push(span_vec1, false, Some(format!("error occurs here"))); + snippet.push(span_vec0, false, Some(format!("previous borrow of `vec` occurs here"))); + snippet.push(span_semi, false, Some(format!("previous borrow ends here"))); + + let lines = snippet.render_lines(); + println!("{:#?}", lines); + let text: String = make_string(&lines); + + println!("text=r#\"\n{}\".trim_left()", text); + assert_eq!(&text[..], &r#" +>>>> foo.rs +3 |> vec.push(vec.pop().unwrap()); + |> --- --- - previous borrow ends here + |> | | + |> | error occurs here + |> previous borrow of `vec` occurs here +"#[1..]); +} + +#[test] +fn elide_unnecessary_lines() { + let file_text = r#" +fn foo() { + let mut vec = vec![0, 1, 2]; + let mut vec2 = vec; + vec2.push(3); + vec2.push(4); + vec2.push(5); + vec2.push(6); + vec.push(7); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + let span_vec0 = cm.span_substr(&foo, file_text, "vec", 3); + let span_vec1 = cm.span_substr(&foo, file_text, "vec", 8); + + let mut snippet = SnippetData::new(cm, None); + snippet.push(span_vec0, false, Some(format!("`vec` moved here because it \ + has type `collections::vec::Vec`"))); + snippet.push(span_vec1, false, Some(format!("use of moved value: `vec`"))); + + let lines = snippet.render_lines(); + println!("{:#?}", lines); + let text: String = make_string(&lines); + println!("text=r#\"\n{}\".trim_left()", text); + assert_eq!(&text[..], &r#" +>>>>>> foo.rs +4 |> let mut vec2 = vec; + |> --- `vec` moved here because it has type `collections::vec::Vec` +... +9 |> vec.push(7); + |> --- use of moved value: `vec` +"#[1..]); +} + +#[test] +fn spans_without_labels() { + let file_text = r#" +fn foo() { + let mut vec = vec![0, 1, 2]; + let mut vec2 = vec; + vec2.push(3); + vec2.push(4); + vec2.push(5); + vec2.push(6); + vec.push(7); +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + + let mut snippet = SnippetData::new(cm.clone(), None); + for i in 0..4 { + let span_veci = cm.span_substr(&foo, file_text, "vec", i); + snippet.push(span_veci, false, None); + } + + let lines = snippet.render_lines(); + let text: String = make_string(&lines); + println!("text=&r#\"\n{}\n\"#[1..]", text); + assert_eq!(text, &r#" +>>>> foo.rs +3 |> let mut vec = vec![0, 1, 2]; + |> --- --- +4 |> let mut vec2 = vec; + |> --- --- +"#[1..]); +} + +#[test] +fn span_long_selection() { + let file_text = r#" +impl SomeTrait for () { + fn foo(x: u32) { + // impl 1 + // impl 2 + // impl 3 + } +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + + let mut snippet = SnippetData::new(cm.clone(), None); + let fn_span = cm.span_substr(&foo, file_text, "fn", 0); + let rbrace_span = cm.span_substr(&foo, file_text, "}", 0); + snippet.push(splice(fn_span, rbrace_span), false, None); + let lines = snippet.render_lines(); + let text: String = make_string(&lines); + println!("r#\"\n{}\"", text); + assert_eq!(text, &r#" +>>>>>> foo.rs +3 |> fn foo(x: u32) { + |> ---------------- +... +"#[1..]); +} + +#[test] +fn span_overlap_label() { + // Test that we don't put `x_span` to the right of its highlight, + // since there is another highlight that overlaps it. + + let file_text = r#" + fn foo(x: u32) { + } +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + + let mut snippet = SnippetData::new(cm.clone(), None); + let fn_span = cm.span_substr(&foo, file_text, "fn foo(x: u32)", 0); + let x_span = cm.span_substr(&foo, file_text, "x", 0); + snippet.push(fn_span, false, Some(format!("fn_span"))); + snippet.push(x_span, false, Some(format!("x_span"))); + let lines = snippet.render_lines(); + let text: String = make_string(&lines); + println!("r#\"\n{}\"", text); + assert_eq!(text, &r#" +>>>> foo.rs +2 |> fn foo(x: u32) { + |> -------------- + |> | | + |> | x_span + |> fn_span +"#[1..]); +} + +#[test] +fn span_overlap_label2() { + // Test that we don't put `x_span` to the right of its highlight, + // since there is another highlight that overlaps it. In this + // case, the overlap is only at the beginning, but it's still + // better to show the beginning more clearly. + + let file_text = r#" + fn foo(x: u32) { + } +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + + let mut snippet = SnippetData::new(cm.clone(), None); + let fn_span = cm.span_substr(&foo, file_text, "fn foo(x", 0); + let x_span = cm.span_substr(&foo, file_text, "x: u32)", 0); + snippet.push(fn_span, false, Some(format!("fn_span"))); + snippet.push(x_span, false, Some(format!("x_span"))); + let lines = snippet.render_lines(); + let text: String = make_string(&lines); + println!("r#\"\n{}\"", text); + assert_eq!(text, &r#" +>>>> foo.rs +2 |> fn foo(x: u32) { + |> -------------- + |> | | + |> | x_span + |> fn_span +"#[1..]); +} + +#[test] +fn span_overlap_label3() { + // Test that we don't put `x_span` to the right of its highlight, + // since there is another highlight that overlaps it. In this + // case, the overlap is only at the beginning, but it's still + // better to show the beginning more clearly. + + let file_text = r#" + fn foo() { + let closure = || { + inner + }; + } +} +"#; + + let cm = Rc::new(CodeMap::new()); + let foo = cm.new_filemap_and_lines("foo.rs", file_text); + + let mut snippet = SnippetData::new(cm.clone(), None); + + let closure_span = { + let closure_start_span = cm.span_substr(&foo, file_text, "||", 0); + let closure_end_span = cm.span_substr(&foo, file_text, "}", 0); + splice(closure_start_span, closure_end_span) + }; + + let inner_span = cm.span_substr(&foo, file_text, "inner", 0); + + snippet.push(closure_span, false, Some(format!("foo"))); + snippet.push(inner_span, false, Some(format!("bar"))); + + let lines = snippet.render_lines(); + let text: String = make_string(&lines); + println!("r#\"\n{}\"", text); + assert_eq!(text, &r#" +>>>> foo.rs +3 |> let closure = || { + |> ---- foo +4 |> inner + |> ---------------- + |> | + |> bar +5 |> }; + |> -------- +"#[1..]); +}