Refactor suggestion diagnostic API to allow for multiple suggestions

This commit is contained in:
Oliver Schneider 2017-05-09 10:04:24 +02:00
parent 58b33ad70c
commit 67d762d896
No known key found for this signature in database
GPG key ID: A69F8D225B3AD7D9
5 changed files with 148 additions and 101 deletions

View file

@ -23,7 +23,7 @@ pub struct Diagnostic {
pub code: Option<String>,
pub span: MultiSpan,
pub children: Vec<SubDiagnostic>,
pub suggestion: Option<CodeSuggestion>,
pub suggestions: Vec<CodeSuggestion>,
}
/// For example a note attached to an error.
@ -87,7 +87,7 @@ impl Diagnostic {
code: code,
span: MultiSpan::new(),
children: vec![],
suggestion: None,
suggestions: vec![],
}
}
@ -204,10 +204,16 @@ impl Diagnostic {
///
/// See `diagnostic::CodeSuggestion` for more information.
pub fn span_suggestion(&mut self, sp: Span, msg: &str, suggestion: String) -> &mut Self {
assert!(self.suggestion.is_none());
self.suggestion = Some(CodeSuggestion {
msp: sp.into(),
substitutes: vec![suggestion],
self.suggestions.push(CodeSuggestion {
substitutes: vec![(sp, vec![suggestion])],
msg: msg.to_owned(),
});
self
}
pub fn span_suggestions(&mut self, sp: Span, msg: &str, suggestions: Vec<String>) -> &mut Self {
self.suggestions.push(CodeSuggestion {
substitutes: vec![(sp, suggestions)],
msg: msg.to_owned(),
});
self

View file

@ -148,6 +148,11 @@ impl<'a> DiagnosticBuilder<'a> {
msg: &str,
suggestion: String)
-> &mut Self);
forward!(pub fn span_suggestions(&mut self,
sp: Span,
msg: &str,
suggestions: Vec<String>)
-> &mut Self);
forward!(pub fn set_span<S: Into<MultiSpan>>(&mut self, sp: S) -> &mut Self);
forward!(pub fn code(&mut self, s: String) -> &mut Self);

View file

@ -35,22 +35,37 @@ impl Emitter for EmitterWriter {
let mut primary_span = db.span.clone();
let mut children = db.children.clone();
if let Some(sugg) = db.suggestion.clone() {
assert_eq!(sugg.msp.primary_spans().len(), sugg.substitutes.len());
if db.suggestions.len() == 1 {
let sugg = &db.suggestions[0];
// don't display multispans as labels
if sugg.substitutes.len() == 1 &&
// don't display multi-suggestions as labels
sugg.substitutes[0].1.len() == 1 &&
// don't display long messages as labels
sugg.msg.split_whitespace().count() < 10 &&
// don't display multiline suggestions as labels
sugg.substitutes[0].find('\n').is_none() {
let msg = format!("help: {} `{}`", sugg.msg, sugg.substitutes[0]);
primary_span.push_span_label(sugg.msp.primary_spans()[0], msg);
sugg.substitutes[0].1[0].find('\n').is_none() {
let msg = format!("help: {} `{}`", sugg.msg, sugg.substitutes[0].1[0]);
primary_span.push_span_label(sugg.substitutes[0].0, msg);
} else {
children.push(SubDiagnostic {
level: Level::Help,
message: Vec::new(),
span: MultiSpan::new(),
render_span: Some(Suggestion(sugg)),
render_span: Some(Suggestion(sugg.clone())),
});
}
} else {
// if there are multiple suggestions, print them all in full
// to be consistent. We could try to figure out if we can
// make one (or the first one) inline, but that would give
// undue importance to a semi-random suggestion
for sugg in &db.suggestions {
children.push(SubDiagnostic {
level: Level::Help,
message: Vec::new(),
span: MultiSpan::new(),
render_span: Some(Suggestion(sugg.clone())),
});
}
}
@ -1054,38 +1069,38 @@ impl EmitterWriter {
-> io::Result<()> {
use std::borrow::Borrow;
let primary_span = suggestion.msp.primary_span().unwrap();
let primary_span = suggestion.substitutes[0].0;
if let Some(ref cm) = self.cm {
let mut buffer = StyledBuffer::new();
buffer.append(0, &level.to_string(), Style::Level(level.clone()));
buffer.append(0, ": ", Style::HeaderMsg);
self.msg_to_buffer(&mut buffer,
&[(suggestion.msg.to_owned(), Style::NoStyle)],
max_line_num_len,
"suggestion",
Some(Style::HeaderMsg));
let lines = cm.span_to_lines(primary_span).unwrap();
assert!(!lines.lines.is_empty());
let complete = suggestion.splice_lines(cm.borrow());
for complete in suggestion.splice_lines(cm.borrow()) {
buffer.append(0, &level.to_string(), Style::Level(level.clone()));
buffer.append(0, ": ", Style::HeaderMsg);
self.msg_to_buffer(&mut buffer,
&[(suggestion.msg.to_owned(), Style::NoStyle)],
max_line_num_len,
"suggestion",
Some(Style::HeaderMsg));
// print the suggestion without any line numbers, but leave
// space for them. This helps with lining up with previous
// snippets from the actual error being reported.
let mut lines = complete.lines();
let mut row_num = 1;
for line in lines.by_ref().take(MAX_HIGHLIGHT_LINES) {
draw_col_separator(&mut buffer, row_num, max_line_num_len + 1);
buffer.append(row_num, line, Style::NoStyle);
row_num += 1;
}
// print the suggestion without any line numbers, but leave
// space for them. This helps with lining up with previous
// snippets from the actual error being reported.
let mut lines = complete.lines();
let mut row_num = 1;
for line in lines.by_ref().take(MAX_HIGHLIGHT_LINES) {
draw_col_separator(&mut buffer, row_num, max_line_num_len + 1);
buffer.append(row_num, line, Style::NoStyle);
row_num += 1;
}
// if we elided some lines, add an ellipsis
if let Some(_) = lines.next() {
buffer.append(row_num, "...", Style::NoStyle);
// if we elided some lines, add an ellipsis
if let Some(_) = lines.next() {
buffer.append(row_num, "...", Style::NoStyle);
}
}
emit_to_destination(&buffer.render(), level, &mut self.dst)?;
}

View file

@ -65,8 +65,25 @@ pub enum RenderSpan {
#[derive(Clone, Debug, PartialEq, RustcEncodable, RustcDecodable)]
pub struct CodeSuggestion {
pub msp: MultiSpan,
pub substitutes: Vec<String>,
/// Each substitute can have multiple variants due to multiple
/// applicable suggestions
///
/// `foo.bar` might be replaced with `a.b` or `x.y` by replacing
/// `foo` and `bar` on their own:
///
/// ```
/// vec![
/// (0..3, vec!["a", "x"]),
/// (4..7, vec!["b", "y"]),
/// ]
/// ```
///
/// or by replacing the entire span:
///
/// ```
/// vec![(0..7, vec!["a.b", "x.y"])]
/// ```
pub substitutes: Vec<(Span, Vec<String>)>,
pub msg: String,
}
@ -79,8 +96,8 @@ pub trait CodeMapper {
}
impl CodeSuggestion {
/// Returns the assembled code suggestion.
pub fn splice_lines(&self, cm: &CodeMapper) -> String {
/// Returns the assembled code suggestions.
pub fn splice_lines(&self, cm: &CodeMapper) -> Vec<String> {
use syntax_pos::{CharPos, Loc, Pos};
fn push_trailing(buf: &mut String,
@ -102,20 +119,22 @@ impl CodeSuggestion {
}
}
let mut primary_spans = self.msp.primary_spans().to_owned();
assert_eq!(primary_spans.len(), self.substitutes.len());
if primary_spans.is_empty() {
return format!("");
if self.substitutes.is_empty() {
return vec![String::new()];
}
let mut primary_spans: Vec<_> = self.substitutes
.iter()
.map(|&(sp, ref sub)| (sp, sub))
.collect();
// Assumption: all spans are in the same file, and all spans
// are disjoint. Sort in ascending order.
primary_spans.sort_by_key(|sp| sp.lo);
primary_spans.sort_by_key(|sp| sp.0.lo);
// Find the bounding span.
let lo = primary_spans.iter().map(|sp| sp.lo).min().unwrap();
let hi = primary_spans.iter().map(|sp| sp.hi).min().unwrap();
let lo = primary_spans.iter().map(|sp| sp.0.lo).min().unwrap();
let hi = primary_spans.iter().map(|sp| sp.0.hi).min().unwrap();
let bounding_span = Span {
lo: lo,
hi: hi,
@ -138,33 +157,37 @@ impl CodeSuggestion {
prev_hi.col = CharPos::from_usize(0);
let mut prev_line = fm.get_line(lines.lines[0].line_index);
let mut buf = String::new();
let mut bufs = vec![String::new(); self.substitutes[0].1.len()];
for (sp, substitute) in primary_spans.iter().zip(self.substitutes.iter()) {
for (sp, substitutes) in primary_spans {
let cur_lo = cm.lookup_char_pos(sp.lo);
if prev_hi.line == cur_lo.line {
push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
} else {
push_trailing(&mut buf, prev_line, &prev_hi, None);
// push lines between the previous and current span (if any)
for idx in prev_hi.line..(cur_lo.line - 1) {
if let Some(line) = fm.get_line(idx) {
buf.push_str(line);
buf.push('\n');
for (buf, substitute) in bufs.iter_mut().zip(substitutes) {
if prev_hi.line == cur_lo.line {
push_trailing(buf, prev_line, &prev_hi, Some(&cur_lo));
} else {
push_trailing(buf, prev_line, &prev_hi, None);
// push lines between the previous and current span (if any)
for idx in prev_hi.line..(cur_lo.line - 1) {
if let Some(line) = fm.get_line(idx) {
buf.push_str(line);
buf.push('\n');
}
}
if let Some(cur_line) = fm.get_line(cur_lo.line - 1) {
buf.push_str(&cur_line[..cur_lo.col.to_usize()]);
}
}
if let Some(cur_line) = fm.get_line(cur_lo.line - 1) {
buf.push_str(&cur_line[..cur_lo.col.to_usize()]);
}
buf.push_str(substitute);
}
buf.push_str(substitute);
prev_hi = cm.lookup_char_pos(sp.hi);
prev_line = fm.get_line(prev_hi.line - 1);
}
push_trailing(&mut buf, prev_line, &prev_hi, None);
// remove trailing newline
buf.pop();
buf
for buf in &mut bufs {
push_trailing(buf, prev_line, &prev_hi, None);
// remove trailing newline
buf.pop();
}
bufs
}
}

View file

@ -22,9 +22,8 @@
use codemap::{CodeMap, FilePathMapping};
use syntax_pos::{self, MacroBacktrace, Span, SpanLabel, MultiSpan};
use errors::registry::Registry;
use errors::{Level, DiagnosticBuilder, SubDiagnostic, RenderSpan, CodeSuggestion, CodeMapper};
use errors::{DiagnosticBuilder, SubDiagnostic, RenderSpan, CodeSuggestion, CodeMapper};
use errors::emitter::Emitter;
use errors::snippet::Style;
use std::rc::Rc;
use std::io::{self, Write};
@ -154,23 +153,26 @@ impl Diagnostic {
fn from_diagnostic_builder(db: &DiagnosticBuilder,
je: &JsonEmitter)
-> Diagnostic {
let sugg = db.suggestion.as_ref().map(|sugg| {
SubDiagnostic {
level: Level::Help,
message: vec![(sugg.msg.clone(), Style::NoStyle)],
span: MultiSpan::new(),
render_span: Some(RenderSpan::Suggestion(sugg.clone())),
}
let sugg = db.suggestions.iter().flat_map(|sugg| {
je.render(sugg).into_iter().map(move |rendered| {
Diagnostic {
message: sugg.msg.clone(),
code: None,
level: "help",
spans: DiagnosticSpan::from_suggestion(sugg, je),
children: vec![],
rendered: Some(rendered),
}
})
});
let sugg = sugg.as_ref();
Diagnostic {
message: db.message(),
code: DiagnosticCode::map_opt_string(db.code.clone(), je),
level: db.level.to_str(),
spans: DiagnosticSpan::from_multispan(&db.span, je),
children: db.children.iter().chain(sugg).map(|c| {
children: db.children.iter().map(|c| {
Diagnostic::from_sub_diagnostic(c, je)
}).collect(),
}).chain(sugg).collect(),
rendered: None,
}
}
@ -184,8 +186,7 @@ impl Diagnostic {
.map(|sp| DiagnosticSpan::from_render_span(sp, je))
.unwrap_or_else(|| DiagnosticSpan::from_multispan(&db.span, je)),
children: vec![],
rendered: db.render_span.as_ref()
.and_then(|rsp| je.render(rsp)),
rendered: None,
}
}
}
@ -278,14 +279,19 @@ impl DiagnosticSpan {
fn from_suggestion(suggestion: &CodeSuggestion, je: &JsonEmitter)
-> Vec<DiagnosticSpan> {
assert_eq!(suggestion.msp.span_labels().len(), suggestion.substitutes.len());
suggestion.msp.span_labels()
.into_iter()
.zip(&suggestion.substitutes)
.map(|(span_label, suggestion)| {
DiagnosticSpan::from_span_label(span_label,
Some(suggestion),
je)
suggestion.substitutes
.iter()
.flat_map(|&(span, ref suggestion)| {
suggestion.iter().map(move |suggestion| {
let span_label = SpanLabel {
span,
is_primary: true,
label: None,
};
DiagnosticSpan::from_span_label(span_label,
Some(suggestion),
je)
})
})
.collect()
}
@ -294,8 +300,9 @@ impl DiagnosticSpan {
match *rsp {
RenderSpan::FullSpan(ref msp) =>
DiagnosticSpan::from_multispan(msp, je),
RenderSpan::Suggestion(ref suggestion) =>
DiagnosticSpan::from_suggestion(suggestion, je),
// regular diagnostics don't produce this anymore
// will be removed in a later commit
RenderSpan::Suggestion(_) => unreachable!(),
}
}
}
@ -351,17 +358,8 @@ impl DiagnosticCode {
}
impl JsonEmitter {
fn render(&self, render_span: &RenderSpan) -> Option<String> {
use std::borrow::Borrow;
match *render_span {
RenderSpan::FullSpan(_) => {
None
}
RenderSpan::Suggestion(ref suggestion) => {
Some(suggestion.splice_lines(self.cm.borrow()))
}
}
fn render(&self, suggestion: &CodeSuggestion) -> Vec<String> {
suggestion.splice_lines(&*self.cm)
}
}