Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 1e60c61

Browse files
authored
Merge pull request rust-lang#3045 from scampi/issue2917
Prevent right-shifting of block comments with bare lines.
2 parents ff4fe5d + 5fdb6db commit 1e60c61

File tree

5 files changed

+345
-88
lines changed

5 files changed

+345
-88
lines changed

src/comment.rs

Lines changed: 194 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@ fn custom_opener(s: &str) -> &str {
5252
}
5353

5454
impl<'a> CommentStyle<'a> {
55+
/// Returns true if the commenting style covers a line only.
56+
pub fn is_line_comment(&self) -> bool {
57+
match *self {
58+
CommentStyle::DoubleSlash
59+
| CommentStyle::TripleSlash
60+
| CommentStyle::Doc
61+
| CommentStyle::Custom(_) => true,
62+
_ => false,
63+
}
64+
}
65+
66+
/// Returns true if the commenting style can span over multiple lines.
67+
pub fn is_block_comment(&self) -> bool {
68+
match *self {
69+
CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => {
70+
true
71+
}
72+
_ => false,
73+
}
74+
}
75+
5576
pub fn is_doc_comment(&self) -> bool {
5677
match *self {
5778
CommentStyle::TripleSlash | CommentStyle::Doc => true,
@@ -213,7 +234,7 @@ pub fn combine_strs_with_missing_comments(
213234
}
214235

215236
pub fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> Option<String> {
216-
_rewrite_comment(orig, false, shape, config, true)
237+
identify_comment(orig, false, shape, config, true)
217238
}
218239

219240
pub fn rewrite_comment(
@@ -222,32 +243,7 @@ pub fn rewrite_comment(
222243
shape: Shape,
223244
config: &Config,
224245
) -> Option<String> {
225-
_rewrite_comment(orig, block_style, shape, config, false)
226-
}
227-
228-
fn _rewrite_comment(
229-
orig: &str,
230-
block_style: bool,
231-
shape: Shape,
232-
config: &Config,
233-
is_doc_comment: bool,
234-
) -> Option<String> {
235-
// If there are lines without a starting sigil, we won't format them correctly
236-
// so in that case we won't even re-align (if !config.normalize_comments()) and
237-
// we should stop now.
238-
let num_bare_lines = orig
239-
.lines()
240-
.map(|line| line.trim())
241-
.filter(|l| !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*")))
242-
.count();
243-
if num_bare_lines > 0 && !config.normalize_comments() {
244-
return Some(orig.to_owned());
245-
}
246-
if !config.normalize_comments() && !config.wrap_comments() {
247-
return light_rewrite_comment(orig, shape.indent, config, is_doc_comment);
248-
}
249-
250-
identify_comment(orig, block_style, shape, config, is_doc_comment)
246+
identify_comment(orig, block_style, shape, config, false)
251247
}
252248

253249
fn identify_comment(
@@ -258,8 +254,8 @@ fn identify_comment(
258254
is_doc_comment: bool,
259255
) -> Option<String> {
260256
let style = comment_style(orig, false);
261-
let mut first_group_ending = 0;
262257

258+
// Computes the len of line taking into account a newline if the line is part of a paragraph.
263259
fn compute_len(orig: &str, line: &str) -> usize {
264260
if orig.len() > line.len() {
265261
if orig.as_bytes()[line.len()] == b'\r' {
@@ -272,62 +268,145 @@ fn identify_comment(
272268
}
273269
}
274270

275-
match style {
271+
// Get the first group of line comments having the same commenting style.
272+
//
273+
// Returns a tuple with:
274+
// - a boolean indicating if there is a blank line
275+
// - a number indicating the size of the first group of comments
276+
fn consume_same_line_comments(
277+
style: CommentStyle,
278+
orig: &str,
279+
line_start: &str,
280+
) -> (bool, usize) {
281+
let mut first_group_ending = 0;
282+
let mut hbl = false;
283+
284+
for line in orig.lines() {
285+
let trimmed_line = line.trim_left();
286+
if trimmed_line.is_empty() {
287+
hbl = true;
288+
break;
289+
} else if trimmed_line.starts_with(line_start)
290+
|| comment_style(trimmed_line, false) == style
291+
{
292+
first_group_ending += compute_len(&orig[first_group_ending..], line);
293+
} else {
294+
break;
295+
}
296+
}
297+
(hbl, first_group_ending)
298+
}
299+
300+
let (has_bare_lines, first_group_ending) = match style {
276301
CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => {
277302
let line_start = style.line_start().trim_left();
278-
for line in orig.lines() {
279-
if line.trim_left().starts_with(line_start) || comment_style(line, false) == style {
280-
first_group_ending += compute_len(&orig[first_group_ending..], line);
281-
} else {
282-
break;
283-
}
284-
}
303+
consume_same_line_comments(style, orig, line_start)
285304
}
286305
CommentStyle::Custom(opener) => {
287306
let trimmed_opener = opener.trim_right();
288-
for line in orig.lines() {
289-
if line.trim_left().starts_with(trimmed_opener) {
290-
first_group_ending += compute_len(&orig[first_group_ending..], line);
291-
} else {
292-
break;
293-
}
294-
}
307+
consume_same_line_comments(style, orig, trimmed_opener)
295308
}
296309
// for a block comment, search for the closing symbol
297310
CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => {
298311
let closer = style.closer().trim_left();
312+
let mut closing_symbol_offset = 0;
313+
let mut hbl = false;
299314
for line in orig.lines() {
300-
first_group_ending += compute_len(&orig[first_group_ending..], line);
301-
if line.trim_left().ends_with(closer) {
315+
closing_symbol_offset += compute_len(&orig[closing_symbol_offset..], line);
316+
let trimmed_line = line.trim_left();
317+
if !trimmed_line.starts_with('*')
318+
&& !trimmed_line.starts_with("//")
319+
&& !trimmed_line.starts_with("/*")
320+
{
321+
hbl = true;
322+
}
323+
if trimmed_line.ends_with(closer) {
302324
break;
303325
}
304326
}
327+
(hbl, closing_symbol_offset)
305328
}
306-
}
329+
};
307330

308331
let (first_group, rest) = orig.split_at(first_group_ending);
309-
let first_group_str = rewrite_comment_inner(
310-
first_group,
311-
block_style,
312-
style,
313-
shape,
314-
config,
315-
is_doc_comment || style.is_doc_comment(),
316-
)?;
332+
let rewritten_first_group =
333+
if !config.normalize_comments() && has_bare_lines && style.is_block_comment() {
334+
light_rewrite_block_comment_with_bare_lines(first_group, shape, config)?
335+
} else if !config.normalize_comments() && !config.wrap_comments() {
336+
light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)?
337+
} else {
338+
rewrite_comment_inner(
339+
first_group,
340+
block_style,
341+
style,
342+
shape,
343+
config,
344+
is_doc_comment || style.is_doc_comment(),
345+
)?
346+
};
317347
if rest.is_empty() {
318-
Some(first_group_str)
348+
Some(rewritten_first_group)
319349
} else {
320-
identify_comment(rest, block_style, shape, config, is_doc_comment).map(|rest_str| {
321-
format!(
322-
"{}\n{}{}",
323-
first_group_str,
324-
shape.indent.to_string(config),
325-
rest_str
326-
)
327-
})
350+
identify_comment(rest.trim_left(), block_style, shape, config, is_doc_comment).map(
351+
|rest_str| {
352+
format!(
353+
"{}\n{}{}{}",
354+
rewritten_first_group,
355+
// insert back the blank line
356+
if has_bare_lines && style.is_line_comment() {
357+
"\n"
358+
} else {
359+
""
360+
},
361+
shape.indent.to_string(config),
362+
rest_str
363+
)
364+
},
365+
)
328366
}
329367
}
330368

369+
/// Trims a minimum of leading whitespaces so that the content layout is kept and aligns to indent.
370+
fn light_rewrite_block_comment_with_bare_lines(
371+
orig: &str,
372+
shape: Shape,
373+
config: &Config,
374+
) -> Option<String> {
375+
let prefix_whitespace_min = orig
376+
.lines()
377+
// skip the line with the starting sigil since the leading whitespace is removed
378+
// otherwise, the minimum would always be zero
379+
.skip(1)
380+
.filter(|line| !line.is_empty())
381+
.map(|line| {
382+
let mut width = 0;
383+
for c in line.chars() {
384+
match c {
385+
' ' => width += 1,
386+
'\t' => width += config.tab_spaces(),
387+
_ => break,
388+
}
389+
}
390+
width
391+
})
392+
.min()?;
393+
394+
let indent_str = shape.indent.to_string(config);
395+
let mut lines = orig.lines();
396+
let first_line = lines.next()?;
397+
let rest = lines
398+
.map(|line| {
399+
if line.is_empty() {
400+
line
401+
} else {
402+
&line[prefix_whitespace_min..]
403+
}
404+
})
405+
.collect::<Vec<&str>>()
406+
.join(&format!("\n{}", indent_str));
407+
Some(format!("{}\n{}{}", first_line, indent_str, rest))
408+
}
409+
331410
fn rewrite_comment_inner(
332411
orig: &str,
333412
block_style: bool,
@@ -1413,26 +1492,29 @@ mod test {
14131492
#[test]
14141493
#[rustfmt::skip]
14151494
fn format_comments() {
1416-
let mut config: ::config::Config = Default::default();
1417-
config.set().wrap_comments(true);
1418-
config.set().normalize_comments(true);
1495+
let mut wrap_normalize_config: ::config::Config = Default::default();
1496+
wrap_normalize_config.set().wrap_comments(true);
1497+
wrap_normalize_config.set().normalize_comments(true);
1498+
1499+
let mut wrap_config: ::config::Config = Default::default();
1500+
wrap_config.set().wrap_comments(true);
14191501

14201502
let comment = rewrite_comment(" //test",
14211503
true,
14221504
Shape::legacy(100, Indent::new(0, 100)),
1423-
&config).unwrap();
1505+
&wrap_normalize_config).unwrap();
14241506
assert_eq!("/* test */", comment);
14251507

14261508
let comment = rewrite_comment("// comment on a",
14271509
false,
14281510
Shape::legacy(10, Indent::empty()),
1429-
&config).unwrap();
1511+
&wrap_normalize_config).unwrap();
14301512
assert_eq!("// comment\n// on a", comment);
14311513

14321514
let comment = rewrite_comment("// A multi line comment\n // between args.",
14331515
false,
14341516
Shape::legacy(60, Indent::new(0, 12)),
1435-
&config).unwrap();
1517+
&wrap_normalize_config).unwrap();
14361518
assert_eq!("// A multi line comment\n // between args.", comment);
14371519

14381520
let input = "// comment";
@@ -1441,14 +1523,55 @@ mod test {
14411523
let comment = rewrite_comment(input,
14421524
true,
14431525
Shape::legacy(9, Indent::new(0, 69)),
1444-
&config).unwrap();
1526+
&wrap_normalize_config).unwrap();
14451527
assert_eq!(expected, comment);
14461528

14471529
let comment = rewrite_comment("/* trimmed */",
14481530
true,
14491531
Shape::legacy(100, Indent::new(0, 100)),
1450-
&config).unwrap();
1532+
&wrap_normalize_config).unwrap();
14511533
assert_eq!("/* trimmed */", comment);
1534+
1535+
// check that different comment style are properly recognised
1536+
let comment = rewrite_comment(r#"/// test1
1537+
/// test2
1538+
/*
1539+
* test3
1540+
*/"#,
1541+
false,
1542+
Shape::legacy(100, Indent::new(0, 0)),
1543+
&wrap_normalize_config).unwrap();
1544+
assert_eq!("/// test1\n/// test2\n// test3", comment);
1545+
1546+
// check that the blank line marks the end of a commented paragraph
1547+
let comment = rewrite_comment(r#"// test1
1548+
1549+
// test2"#,
1550+
false,
1551+
Shape::legacy(100, Indent::new(0, 0)),
1552+
&wrap_normalize_config).unwrap();
1553+
assert_eq!("// test1\n\n// test2", comment);
1554+
1555+
// check that the blank line marks the end of a custom-commented paragraph
1556+
let comment = rewrite_comment(r#"//@ test1
1557+
1558+
//@ test2"#,
1559+
false,
1560+
Shape::legacy(100, Indent::new(0, 0)),
1561+
&wrap_normalize_config).unwrap();
1562+
assert_eq!("//@ test1\n\n//@ test2", comment);
1563+
1564+
// check that bare lines are just indented but left unchanged otherwise
1565+
let comment = rewrite_comment(r#"// test1
1566+
/*
1567+
a bare line!
1568+
1569+
another bare line!
1570+
*/"#,
1571+
false,
1572+
Shape::legacy(100, Indent::new(0, 0)),
1573+
&wrap_config).unwrap();
1574+
assert_eq!("// test1\n/*\n a bare line!\n\n another bare line!\n*/", comment);
14521575
}
14531576

14541577
// This is probably intended to be a non-test fn, but it is not used. I'm

0 commit comments

Comments
 (0)