Refine chain breaking heuristics

Don't make a single line chain when it is was multi line in the source; allow overflow of the last chain element onto the next lines without breaking the chain.
This commit is contained in:
Marcus Klaas 2015-09-09 23:17:31 +02:00
parent 48d17f54d3
commit 03c660633f
18 changed files with 351 additions and 202 deletions

View file

@ -20,7 +20,7 @@
// argument function argument strategy.
use rewrite::{Rewrite, RewriteContext};
use utils::make_indent;
use utils::{first_line_width, make_indent};
use expr::rewrite_call;
use syntax::{ast, ptr};
@ -51,19 +51,67 @@ pub fn rewrite_chain(mut expr: &ast::Expr,
let indent = offset + extra_indent;
let max_width = try_opt!(width.checked_sub(extra_indent));
let rewrites = try_opt!(subexpr_list.into_iter()
.rev()
.map(|e| {
rewrite_chain_expr(e,
total_span,
context,
max_width,
indent)
})
.collect::<Option<Vec<_>>>());
let mut rewrites = try_opt!(subexpr_list.iter()
.rev()
.map(|e| {
rewrite_chain_expr(e,
total_span,
context,
max_width,
indent)
})
.collect::<Option<Vec<_>>>());
let total_width = rewrites.iter().fold(0, |a, b| a + b.len()) + parent_rewrite.len();
let fits_single_line = total_width <= width && rewrites.iter().all(|s| !s.contains('\n'));
// Total of all items excluding the last.
let almost_total = rewrites.split_last()
.unwrap()
.1
.iter()
.fold(0, |a, b| a + first_line_width(b)) +
parent_rewrite.len();
let total_width = almost_total + first_line_width(rewrites.last().unwrap());
let veto_single_line = if context.config.take_source_hints && subexpr_list.len() > 1 {
// Look at the source code. Unless all chain elements start on the same
// line, we won't consider putting them on a single line either.
let first_line_no = context.codemap.lookup_char_pos(subexpr_list[0].span.lo).line;
subexpr_list[1..]
.iter()
.any(|ex| context.codemap.lookup_char_pos(ex.span.hi).line != first_line_no)
} else {
false
};
let fits_single_line = !veto_single_line &&
match subexpr_list[0].node {
ast::Expr_::ExprMethodCall(ref method_name, ref types, ref expressions)
if context.config.chains_overflow_last => {
let (last, init) = rewrites.split_last_mut().unwrap();
if init.iter().all(|s| !s.contains('\n')) && total_width <= width {
let last_rewrite = width.checked_sub(almost_total)
.and_then(|inner_width| {
rewrite_method_call(method_name.node,
types,
expressions,
total_span,
context,
inner_width,
offset + almost_total)
});
match last_rewrite {
Some(mut string) => {
::std::mem::swap(&mut string, last);
true
}
None => false,
}
} else {
false
}
}
_ => total_width <= width && rewrites.iter().all(|s| !s.contains('\n')),
};
let connector = if fits_single_line {
String::new()
@ -101,7 +149,11 @@ fn rewrite_chain_expr(expr: &ast::Expr,
-> Option<String> {
match expr.node {
ast::Expr_::ExprMethodCall(ref method_name, ref types, ref expressions) => {
rewrite_method_call(method_name.node, types, expressions, span, context, width, offset)
let inner = &RewriteContext {
block_indent: offset,
..*context
};
rewrite_method_call(method_name.node, types, expressions, span, inner, width, offset)
}
ast::Expr_::ExprField(_, ref field) => {
Some(format!(".{}", field.node))
@ -137,10 +189,7 @@ fn rewrite_method_call(method_name: ast::Ident,
};
let callee_str = format!(".{}{}", method_name, type_str);
let inner_context = &RewriteContext {
block_indent: offset,
..*context
};
let span = mk_sp(args[0].span.hi, span.hi);
rewrite_call(inner_context, &callee_str, args, span, width, offset)
rewrite_call(context, &callee_str, &args[1..], span, width, offset)
}

View file

@ -133,8 +133,8 @@ create_config! {
fn_args_density: Density,
fn_args_layout: StructLitStyle,
fn_arg_indent: BlockIndentStyle,
where_density: Density, // Should we at least try to put the where clause on the same line as
// the rest of the function decl?
where_density: Density, // Should we at least try to put the where clause on
// the same line as the rest of the function decl?
where_indent: BlockIndentStyle, // Visual will be treated like Tabbed
where_layout: ListTactic,
where_pred_indent: BlockIndentStyle,
@ -147,14 +147,14 @@ create_config! {
report_todo: ReportTactic,
report_fixme: ReportTactic,
reorder_imports: bool, // Alphabetically, case sensitive.
expr_indent_style: BlockIndentStyle,
closure_indent_style: BlockIndentStyle,
single_line_if_else: bool,
format_strings: bool,
chains_overflow_last: bool,
take_source_hints: bool, // Retain some formatting characteristics from
// the source code.
}
impl Default for Config {
fn default() -> Config {
Config {
max_width: 100,
@ -181,11 +181,10 @@ impl Default for Config {
report_todo: ReportTactic::Always,
report_fixme: ReportTactic::Never,
reorder_imports: false,
expr_indent_style: BlockIndentStyle::Tabbed,
closure_indent_style: BlockIndentStyle::Visual,
single_line_if_else: false,
format_strings: true,
chains_overflow_last: true,
take_source_hints: true,
}
}
}

View file

@ -40,6 +40,12 @@ impl Rewrite for ast::Expr {
}
}
ast::Expr_::ExprCall(ref callee, ref args) => {
// FIXME using byte lens instead of char lens (and probably all over the place too)
// 2 is for parens
let max_callee_width = try_opt!(width.checked_sub(2));
let callee_str = try_opt!(callee.rewrite(context, max_callee_width, offset));
let span = mk_sp(callee.span.hi, self.span.hi);
rewrite_call(context, &**callee, args, self.span, width, offset)
}
ast::Expr_::ExprParen(ref subexpr) => {
@ -284,8 +290,10 @@ impl Rewrite for ast::Block {
};
if is_simple_block(self, context.codemap) && prefix.len() < width {
let body =
self.expr.as_ref().unwrap().rewrite(context, width - prefix.len(), offset);
let body = self.expr
.as_ref()
.unwrap()
.rewrite(context, width - prefix.len(), offset);
if let Some(ref expr_str) = body {
let result = format!("{}{{ {} }}", prefix, expr_str);
if result.len() <= width && !result.contains('\n') {
@ -677,15 +685,13 @@ impl Rewrite for ast::Arm {
total_width += (pat_strs.len() - 1) * 3;
let mut vertical = total_width > pat_budget || pat_strs.iter().any(|p| p.contains('\n'));
if !vertical {
if !vertical && context.config.take_source_hints {
// If the patterns were previously stacked, keep them stacked.
// FIXME should be an option.
let pat_span = mk_sp(pats[0].span.lo, pats[pats.len() - 1].span.hi);
let pat_str = context.snippet(pat_span);
vertical = pat_str.find('\n').is_some();
}
let pats_width = if vertical {
pat_strs[pat_strs.len() - 1].len()
} else {
@ -1015,9 +1021,10 @@ fn rewrite_struct_lit<'a>(context: &RewriteContext,
match *item {
StructLitField::Regular(ref field) => field.span.lo,
StructLitField::Base(ref expr) => {
let last_field_hi = fields.last()
.map_or(span.lo,
|field| field.span.hi);
let last_field_hi = fields.last().map_or(span.lo,
|field| {
field.span.hi
});
let snippet = context.snippet(mk_sp(last_field_hi,
expr.span.lo));
let pos = snippet.find_uncommented("..").unwrap();

View file

@ -512,8 +512,9 @@ impl<'a> FmtVisitor<'a> {
|arg| arg.ty.span.hi,
|arg| {
// FIXME silly width, indent
arg.ty.rewrite(&self.get_context(), 1000, 0)
.unwrap()
arg.ty
.rewrite(&self.get_context(), 1000, 0)
.unwrap()
},
span_after(field.span, "(", self.codemap),
next_span_start);
@ -810,15 +811,14 @@ impl<'a> FmtVisitor<'a> {
.map(|ty_param| ty_param.rewrite(&context, h_budget, offset).unwrap());
// Extract comments between generics.
let lt_spans = lifetimes.iter()
.map(|l| {
let hi = if l.bounds.is_empty() {
l.lifetime.span.hi
} else {
l.bounds[l.bounds.len() - 1].span.hi
};
codemap::mk_sp(l.lifetime.span.lo, hi)
});
let lt_spans = lifetimes.iter().map(|l| {
let hi = if l.bounds.is_empty() {
l.lifetime.span.hi
} else {
l.bounds[l.bounds.len() - 1].span.hi
};
codemap::mk_sp(l.lifetime.span.lo, hi)
});
let ty_spans = tys.iter().map(span_for_ty_param);
let items = itemize_list(self.codemap,

View file

@ -10,6 +10,7 @@
#![feature(rustc_private)]
#![feature(custom_attribute)]
#![feature(slice_splits)]
#![allow(unused_attributes)]
// TODO we're going to allocate a whole bunch of temp Strings, is it worth

View file

@ -269,109 +269,107 @@ impl<'a, T, I, F1, F2, F3> Iterator for ListItems<'a, I, F1, F2, F3>
fn next(&mut self) -> Option<Self::Item> {
let white_space: &[_] = &[' ', '\t'];
self.inner
.next()
.map(|item| {
let mut new_lines = false;
// Pre-comment
let pre_snippet = self.codemap
.span_to_snippet(codemap::mk_sp(self.prev_span_end,
(self.get_lo)(&item)))
.unwrap();
let trimmed_pre_snippet = pre_snippet.trim();
let pre_comment = if !trimmed_pre_snippet.is_empty() {
Some(trimmed_pre_snippet.to_owned())
} else {
None
};
self.inner.next().map(|item| {
let mut new_lines = false;
// Pre-comment
let pre_snippet = self.codemap
.span_to_snippet(codemap::mk_sp(self.prev_span_end,
(self.get_lo)(&item)))
.unwrap();
let trimmed_pre_snippet = pre_snippet.trim();
let pre_comment = if !trimmed_pre_snippet.is_empty() {
Some(trimmed_pre_snippet.to_owned())
} else {
None
};
// Post-comment
let next_start = match self.inner.peek() {
Some(ref next_item) => (self.get_lo)(next_item),
None => self.next_span_start,
};
let post_snippet = self.codemap
.span_to_snippet(codemap::mk_sp((self.get_hi)(&item),
next_start))
.unwrap();
// Post-comment
let next_start = match self.inner.peek() {
Some(ref next_item) => (self.get_lo)(next_item),
None => self.next_span_start,
};
let post_snippet = self.codemap
.span_to_snippet(codemap::mk_sp((self.get_hi)(&item),
next_start))
.unwrap();
let comment_end = match self.inner.peek() {
Some(..) => {
let block_open_index = post_snippet.find("/*");
let newline_index = post_snippet.find('\n');
let separator_index = post_snippet.find_uncommented(",").unwrap();
let comment_end = match self.inner.peek() {
Some(..) => {
let block_open_index = post_snippet.find("/*");
let newline_index = post_snippet.find('\n');
let separator_index = post_snippet.find_uncommented(",").unwrap();
match (block_open_index, newline_index) {
// Separator before comment, with the next item on same line.
// Comment belongs to next item.
(Some(i), None) if i > separator_index => {
separator_index + 1
}
// Block-style post-comment before the separator.
(Some(i), None) => {
cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
separator_index + 1)
}
// Block-style post-comment. Either before or after the separator.
(Some(i), Some(j)) if i < j => {
cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
separator_index + 1)
}
// Potential *single* line comment.
(_, Some(j)) => j + 1,
_ => post_snippet.len(),
match (block_open_index, newline_index) {
// Separator before comment, with the next item on same line.
// Comment belongs to next item.
(Some(i), None) if i > separator_index => {
separator_index + 1
}
}
None => {
post_snippet.find_uncommented(self.terminator).unwrap_or(post_snippet.len())
}
};
if !post_snippet.is_empty() && comment_end > 0 {
// Account for extra whitespace between items. This is fiddly
// because of the way we divide pre- and post- comments.
// Everything from the separator to the next item.
let test_snippet = &post_snippet[comment_end-1..];
let first_newline = test_snippet.find('\n').unwrap_or(test_snippet.len());
// From the end of the first line of comments.
let test_snippet = &test_snippet[first_newline..];
let first = test_snippet.find(|c: char| !c.is_whitespace())
.unwrap_or(test_snippet.len());
// From the end of the first line of comments to the next non-whitespace char.
let test_snippet = &test_snippet[..first];
if test_snippet.chars().filter(|c| c == &'\n').count() > 1 {
// There were multiple line breaks which got trimmed to nothing.
new_lines = true;
// Block-style post-comment before the separator.
(Some(i), None) => {
cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
separator_index + 1)
}
// Block-style post-comment. Either before or after the separator.
(Some(i), Some(j)) if i < j => {
cmp::max(find_comment_end(&post_snippet[i..]).unwrap() + i,
separator_index + 1)
}
// Potential *single* line comment.
(_, Some(j)) => j + 1,
_ => post_snippet.len(),
}
}
// Cleanup post-comment: strip separators and whitespace.
self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32);
let post_snippet = post_snippet[..comment_end].trim();
let post_snippet_trimmed = if post_snippet.starts_with(',') {
post_snippet[1..].trim_matches(white_space)
} else if post_snippet.ends_with(",") {
post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space)
} else {
post_snippet
};
let post_comment = if !post_snippet_trimmed.is_empty() {
Some(post_snippet_trimmed.to_owned())
} else {
None
};
ListItem {
pre_comment: pre_comment,
item: (self.get_item_string)(&item),
post_comment: post_comment,
new_lines: new_lines,
None => {
post_snippet.find_uncommented(self.terminator).unwrap_or(post_snippet.len())
}
})
};
if !post_snippet.is_empty() && comment_end > 0 {
// Account for extra whitespace between items. This is fiddly
// because of the way we divide pre- and post- comments.
// Everything from the separator to the next item.
let test_snippet = &post_snippet[comment_end-1..];
let first_newline = test_snippet.find('\n').unwrap_or(test_snippet.len());
// From the end of the first line of comments.
let test_snippet = &test_snippet[first_newline..];
let first = test_snippet.find(|c: char| !c.is_whitespace())
.unwrap_or(test_snippet.len());
// From the end of the first line of comments to the next non-whitespace char.
let test_snippet = &test_snippet[..first];
if test_snippet.chars().filter(|c| c == &'\n').count() > 1 {
// There were multiple line breaks which got trimmed to nothing.
new_lines = true;
}
}
// Cleanup post-comment: strip separators and whitespace.
self.prev_span_end = (self.get_hi)(&item) + BytePos(comment_end as u32);
let post_snippet = post_snippet[..comment_end].trim();
let post_snippet_trimmed = if post_snippet.starts_with(',') {
post_snippet[1..].trim_matches(white_space)
} else if post_snippet.ends_with(",") {
post_snippet[..(post_snippet.len() - 1)].trim_matches(white_space)
} else {
post_snippet
};
let post_comment = if !post_snippet_trimmed.is_empty() {
Some(post_snippet_trimmed.to_owned())
} else {
None
};
ListItem {
pre_comment: pre_comment,
item: (self.get_item_string)(&item),
post_comment: post_comment,
new_lines: new_lines,
}
})
}
}

View file

@ -130,16 +130,16 @@ impl<'a> Rewrite for SegmentParam<'a> {
// FIXME doesn't always use width, offset
fn rewrite(&self, context: &RewriteContext, width: usize, offset: usize) -> Option<String> {
Some(match *self {
SegmentParam::LifeTime(ref lt) => {
pprust::lifetime_to_string(lt)
}
SegmentParam::Type(ref ty) => {
try_opt!(ty.rewrite(context, width, offset))
}
SegmentParam::Binding(ref binding) => {
format!("{} = {}", binding.ident, try_opt!(binding.ty.rewrite(context, width, offset)))
}
})
SegmentParam::LifeTime(ref lt) => {
pprust::lifetime_to_string(lt)
}
SegmentParam::Type(ref ty) => {
try_opt!(ty.rewrite(context, width, offset))
}
SegmentParam::Binding(ref binding) => {
format!("{} = {}", binding.ident, try_opt!(binding.ty.rewrite(context, width, offset)))
}
})
}
}
@ -368,8 +368,9 @@ impl Rewrite for ast::TyParamBound {
impl Rewrite for ast::TyParamBounds {
fn rewrite(&self, context: &RewriteContext, width: usize, offset: usize) -> Option<String> {
let strs: Vec<_> =
self.iter().map(|b| b.rewrite(context, width, offset).unwrap()).collect();
let strs: Vec<_> = self.iter()
.map(|b| b.rewrite(context, width, offset).unwrap())
.collect();
Some(strs.join(" + "))
}
}

View file

@ -21,7 +21,7 @@ enum_trailing_comma = true
report_todo = "Always"
report_fixme = "Never"
reorder_imports = false
expr_indent_style = "Tabbed"
closure_indent_style = "Visual"
single_line_if_else = false
format_strings = true
chains_overflow_last = true
take_source_hints = true

View file

@ -0,0 +1,38 @@
// rustfmt-chains_overflow_last: false
// Test chain formatting without overflowing the last item.
fn main() {
bbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccc
.ddddddddddddddddddddddddddd();
bbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddd.eeeeeeee();
x()
.y(|| match cond() { true => (), false => () });
loong_func()
.quux(move || if true {
1
} else {
2
});
fffffffffffffffffffffffffffffffffff(a,
{
SCRIPT_TASK_ROOT
.with(|root| {
// Another case of write_list failing us.
*root.borrow_mut() = Some(&script_task);
});
});
let suuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuum = xxxxxxx
.map(|x| x + 5)
.map(|x| x / 2)
.fold(0, |acc, x| acc + x);
aaaaaaaaaaaaaaaa.map(|x| {
x += 1;
x
}).filter(some_mod::some_filter)
}

View file

@ -1,10 +1,10 @@
// Test chain formatting.
fn main() {
let a = b.c
.d
.1
.foo(|x| x + 1);
// Don't put chains on a single list if it wasn't so in source.
let a = b .c
.d.1
.foo(|x| x + 1);
bbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccc
.ddddddddddddddddddddddddddd();
@ -25,7 +25,6 @@ fn main() {
{
SCRIPT_TASK_ROOT
.with(|root| {
// Another case of write_list failing us.
*root.borrow_mut() = Some(&script_task);
});
});

View file

@ -0,0 +1,8 @@
// rustfmt-take_source_hints: false
// We know best!
fn main() {
a.b
.c
.d();
}

View file

@ -1,9 +0,0 @@
// rustfmt-expr_indent_style: Visual
// Visual level block indentation.
fn matcher() {
Some(while true {
test();
})
}

View file

@ -165,12 +165,10 @@ fn read_significant_comments(file_name: &str) -> HashMap<String, String> {
.map(|line| line.ok().expect("Failed getting line."))
.take_while(|line| line_regex.is_match(&line))
.filter_map(|line| {
regex.captures_iter(&line)
.next()
.map(|capture| {
(capture.at(1).expect("Couldn\'t unwrap capture.").to_owned(),
capture.at(2).expect("Couldn\'t unwrap capture.").to_owned())
})
regex.captures_iter(&line).next().map(|capture| {
(capture.at(1).expect("Couldn\'t unwrap capture.").to_owned(),
capture.at(2).expect("Couldn\'t unwrap capture.").to_owned())
})
})
.collect()
}

View file

@ -0,0 +1,45 @@
// rustfmt-chains_overflow_last: false
// Test chain formatting without overflowing the last item.
fn main() {
bbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddd();
bbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccc
.ddddddddddddddddddddddddddd
.eeeeeeee();
x().y(|| {
match cond() {
true => (),
false => (),
}
});
loong_func()
.quux(move || {
if true {
1
} else {
2
}
});
fffffffffffffffffffffffffffffffffff(a,
{
SCRIPT_TASK_ROOT.with(|root| {
// Another case of write_list failing us.
*root.borrow_mut() = Some(&script_task);
});
});
let suuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuum = xxxxxxx.map(|x| x + 5)
.map(|x| x / 2)
.fold(0,
|acc, x| acc + x);
aaaaaaaaaaaaaaaa.map(|x| {
x += 1;
x
})
.filter(some_mod::some_filter)
}

View file

@ -0,0 +1,16 @@
// rustfmt-chains_overflow_last: false
fn main() {
reader.lines()
.map(|line| line.ok().expect("Failed getting line."))
.take_while(|line| line_regex.is_match(&line))
.filter_map(|line| {
regex.captures_iter(&line)
.next()
.map(|capture| {
(capture.at(1).expect("Couldn\'t unwrap capture.").to_owned(),
capture.at(2).expect("Couldn\'t unwrap capture.").to_owned())
})
})
.collect();
}

View file

@ -1,7 +1,11 @@
// Test chain formatting.
fn main() {
let a = b.c.d.1.foo(|x| x + 1);
// Don't put chains on a single list if it wasn't so in source.
let a = b.c
.d
.1
.foo(|x| x + 1);
bbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddd();
@ -10,27 +14,25 @@ fn main() {
.eeeeeeee();
x().y(|| {
match cond() {
true => (),
false => (),
}
});
match cond() {
true => (),
false => (),
}
});
loong_func()
.quux(move || {
if true {
1
} else {
2
}
});
loong_func().quux(move || {
if true {
1
} else {
2
}
});
fffffffffffffffffffffffffffffffffff(a,
{
SCRIPT_TASK_ROOT.with(|root| {
// Another case of write_list failing us.
*root.borrow_mut() = Some(&script_task);
});
*root.borrow_mut() = Some(&script_task);
});
});
let suuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuum = xxxxxxx.map(|x| x + 5)

View file

@ -0,0 +1,6 @@
// rustfmt-take_source_hints: false
// We know best!
fn main() {
a.b.c.d();
}

View file

@ -1,9 +0,0 @@
// rustfmt-expr_indent_style: Visual
// Visual level block indentation.
fn matcher() {
Some(while true {
test();
})
}