Skip to content

Commit fbeabe9

Browse files
authored
Merge pull request #3113 from scampi/issue3105
Fix handling of code that is annotated with rustfmt::skip.
2 parents 4789f65 + 2f5d864 commit fbeabe9

File tree

7 files changed

+196
-81
lines changed

7 files changed

+196
-81
lines changed

src/comment.rs

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use config::Config;
1919
use rewrite::RewriteContext;
2020
use shape::{Indent, Shape};
2121
use string::{rewrite_string, StringFormat};
22-
use utils::{count_newlines, first_line_width, last_line_width};
22+
use utils::{count_newlines, first_line_width, last_line_width, trim_left_preserve_layout};
2323
use {ErrorKind, FormattingError};
2424

2525
fn is_custom_comment(comment: &str) -> bool {
@@ -332,12 +332,12 @@ fn identify_comment(
332332
let (first_group, rest) = orig.split_at(first_group_ending);
333333
let rewritten_first_group =
334334
if !config.normalize_comments() && has_bare_lines && style.is_block_comment() {
335-
light_rewrite_block_comment_with_bare_lines(first_group, shape, config)?
335+
trim_left_preserve_layout(first_group, &shape.indent, config)
336336
} else if !config.normalize_comments()
337337
&& !config.wrap_comments()
338338
&& !config.format_doc_comments()
339339
{
340-
light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)?
340+
light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)
341341
} else {
342342
rewrite_comment_inner(
343343
first_group,
@@ -370,47 +370,6 @@ fn identify_comment(
370370
}
371371
}
372372

373-
/// Trims a minimum of leading whitespaces so that the content layout is kept and aligns to indent.
374-
fn light_rewrite_block_comment_with_bare_lines(
375-
orig: &str,
376-
shape: Shape,
377-
config: &Config,
378-
) -> Option<String> {
379-
let prefix_whitespace_min = orig
380-
.lines()
381-
// skip the line with the starting sigil since the leading whitespace is removed
382-
// otherwise, the minimum would always be zero
383-
.skip(1)
384-
.filter(|line| !line.is_empty())
385-
.map(|line| {
386-
let mut width = 0;
387-
for c in line.chars() {
388-
match c {
389-
' ' => width += 1,
390-
'\t' => width += config.tab_spaces(),
391-
_ => break,
392-
}
393-
}
394-
width
395-
})
396-
.min()?;
397-
398-
let indent_str = shape.indent.to_string(config);
399-
let mut lines = orig.lines();
400-
let first_line = lines.next()?;
401-
let rest = lines
402-
.map(|line| {
403-
if line.is_empty() {
404-
line
405-
} else {
406-
&line[prefix_whitespace_min..]
407-
}
408-
})
409-
.collect::<Vec<&str>>()
410-
.join(&format!("\n{}", indent_str));
411-
Some(format!("{}\n{}{}", first_line, indent_str, rest))
412-
}
413-
414373
/// Attributes for code blocks in rustdoc.
415374
/// See https://doc.rust-lang.org/rustdoc/print.html#attributes
416375
enum CodeBlockAttribute {
@@ -661,7 +620,7 @@ impl<'a> CommentRewrite<'a> {
661620
let mut config = self.fmt.config.clone();
662621
config.set().format_doc_comments(false);
663622
match ::format_code_block(&self.code_block_buffer, &config) {
664-
Some(ref s) => trim_custom_comment_prefix(s),
623+
Some(ref s) => trim_custom_comment_prefix(&s.snippet),
665624
None => trim_custom_comment_prefix(&self.code_block_buffer),
666625
}
667626
}
@@ -912,7 +871,7 @@ fn light_rewrite_comment(
912871
offset: Indent,
913872
config: &Config,
914873
is_doc_comment: bool,
915-
) -> Option<String> {
874+
) -> String {
916875
let lines: Vec<&str> = orig
917876
.lines()
918877
.map(|l| {
@@ -933,7 +892,7 @@ fn light_rewrite_comment(
933892
trim_right_unless_two_whitespaces(left_trimmed, is_doc_comment)
934893
})
935894
.collect();
936-
Some(lines.join(&format!("\n{}", offset.to_string(config))))
895+
lines.join(&format!("\n{}", offset.to_string(config)))
937896
}
938897

939898
/// Trims comment characters and possibly a single space from the left of a string.

src/formatting.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> {
173173
if visitor.macro_rewrite_failure {
174174
self.report.add_macro_format_failure();
175175
}
176+
self.report
177+
.add_non_formatted_ranges(visitor.skipped_range.clone());
176178

177179
self.handler
178180
.handle_formatted_file(path, visitor.buffer.to_owned(), &mut self.report)

src/lib.rs

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,56 @@ impl From<io::Error> for ErrorKind {
144144
}
145145
}
146146

147+
/// Result of formatting a snippet of code along with ranges of lines that didn't get formatted,
148+
/// i.e., that got returned as they were originally.
149+
#[derive(Debug)]
150+
struct FormattedSnippet {
151+
snippet: String,
152+
non_formatted_ranges: Vec<(usize, usize)>,
153+
}
154+
155+
impl FormattedSnippet {
156+
/// In case the snippet needed to be wrapped in a function, this shifts down the ranges of
157+
/// non-formatted code.
158+
fn unwrap_code_block(&mut self) {
159+
self.non_formatted_ranges
160+
.iter_mut()
161+
.for_each(|(low, high)| {
162+
*low -= 1;
163+
*high -= 1;
164+
});
165+
}
166+
167+
/// Returns true if the line n did not get formatted.
168+
fn is_line_non_formatted(&self, n: usize) -> bool {
169+
self.non_formatted_ranges
170+
.iter()
171+
.any(|(low, high)| *low <= n && n <= *high)
172+
}
173+
}
174+
147175
/// Reports on any issues that occurred during a run of Rustfmt.
148176
///
149177
/// Can be reported to the user via its `Display` implementation of `print_fancy`.
150178
#[derive(Clone)]
151179
pub struct FormatReport {
152180
// Maps stringified file paths to their associated formatting errors.
153181
internal: Rc<RefCell<(FormatErrorMap, ReportedErrors)>>,
182+
non_formatted_ranges: Vec<(usize, usize)>,
154183
}
155184

156185
impl FormatReport {
157186
fn new() -> FormatReport {
158187
FormatReport {
159188
internal: Rc::new(RefCell::new((HashMap::new(), ReportedErrors::default()))),
189+
non_formatted_ranges: Vec::new(),
160190
}
161191
}
162192

193+
fn add_non_formatted_ranges(&mut self, mut ranges: Vec<(usize, usize)>) {
194+
self.non_formatted_ranges.append(&mut ranges);
195+
}
196+
163197
fn append(&self, f: FileName, mut v: Vec<FormattingError>) {
164198
self.track_errors(&v);
165199
self.internal
@@ -349,37 +383,44 @@ impl fmt::Display for FormatReport {
349383

350384
/// Format the given snippet. The snippet is expected to be *complete* code.
351385
/// When we cannot parse the given snippet, this function returns `None`.
352-
fn format_snippet(snippet: &str, config: &Config) -> Option<String> {
386+
fn format_snippet(snippet: &str, config: &Config) -> Option<FormattedSnippet> {
353387
let mut config = config.clone();
354-
let out = panic::catch_unwind(|| {
388+
panic::catch_unwind(|| {
355389
let mut out: Vec<u8> = Vec::with_capacity(snippet.len() * 2);
356390
config.set().emit_mode(config::EmitMode::Stdout);
357391
config.set().verbose(Verbosity::Quiet);
358392
config.set().hide_parse_errors(true);
359-
let formatting_error = {
393+
394+
let (formatting_error, result) = {
360395
let input = Input::Text(snippet.into());
361396
let mut session = Session::new(config, Some(&mut out));
362397
let result = session.format(input);
363-
session.errors.has_macro_format_failure
364-
|| session.out.as_ref().unwrap().is_empty() && !snippet.is_empty()
365-
|| result.is_err()
398+
(
399+
session.errors.has_macro_format_failure
400+
|| session.out.as_ref().unwrap().is_empty() && !snippet.is_empty()
401+
|| result.is_err(),
402+
result,
403+
)
366404
};
367405
if formatting_error {
368406
None
369407
} else {
370-
Some(out)
408+
String::from_utf8(out).ok().map(|snippet| FormattedSnippet {
409+
snippet,
410+
non_formatted_ranges: result.unwrap().non_formatted_ranges,
411+
})
371412
}
372413
})
373-
.ok()??; // The first try operator handles the error from catch_unwind,
374-
// whereas the second one handles None from the closure.
375-
String::from_utf8(out).ok()
414+
// Discard panics encountered while formatting the snippet
415+
// The ? operator is needed to remove the extra Option
416+
.ok()?
376417
}
377418

378419
/// Format the given code block. Mainly targeted for code block in comment.
379420
/// The code block may be incomplete (i.e. parser may be unable to parse it).
380421
/// To avoid panic in parser, we wrap the code block with a dummy function.
381422
/// The returned code block does *not* end with newline.
382-
fn format_code_block(code_snippet: &str, config: &Config) -> Option<String> {
423+
fn format_code_block(code_snippet: &str, config: &Config) -> Option<FormattedSnippet> {
383424
const FN_MAIN_PREFIX: &str = "fn main() {\n";
384425

385426
fn enclose_in_main_block(s: &str, config: &Config) -> String {
@@ -412,13 +453,18 @@ fn format_code_block(code_snippet: &str, config: &Config) -> Option<String> {
412453
config_with_unix_newline
413454
.set()
414455
.newline_style(NewlineStyle::Unix);
415-
let formatted = format_snippet(&snippet, &config_with_unix_newline)?;
456+
let mut formatted = format_snippet(&snippet, &config_with_unix_newline)?;
457+
// Remove wrapping main block
458+
formatted.unwrap_code_block();
416459

417460
// Trim "fn main() {" on the first line and "}" on the last line,
418461
// then unindent the whole code block.
419-
let block_len = formatted.rfind('}').unwrap_or(formatted.len());
462+
let block_len = formatted
463+
.snippet
464+
.rfind('}')
465+
.unwrap_or(formatted.snippet.len());
420466
let mut is_indented = true;
421-
for (kind, ref line) in LineClasses::new(&formatted[FN_MAIN_PREFIX.len()..block_len]) {
467+
for (kind, ref line) in LineClasses::new(&formatted.snippet[FN_MAIN_PREFIX.len()..block_len]) {
422468
if !is_first {
423469
result.push('\n');
424470
} else {
@@ -451,7 +497,10 @@ fn format_code_block(code_snippet: &str, config: &Config) -> Option<String> {
451497
result.push_str(trimmed_line);
452498
is_indented = !kind.is_string() || line.ends_with('\\');
453499
}
454-
Some(result)
500+
Some(FormattedSnippet {
501+
snippet: result,
502+
non_formatted_ranges: formatted.non_formatted_ranges,
503+
})
455504
}
456505

457506
/// A session is a run of rustfmt across a single or multiple inputs.
@@ -571,10 +620,10 @@ mod unit_tests {
571620

572621
fn test_format_inner<F>(formatter: F, input: &str, expected: &str) -> bool
573622
where
574-
F: Fn(&str, &Config) -> Option<String>,
623+
F: Fn(&str, &Config) -> Option<FormattedSnippet>,
575624
{
576625
let output = formatter(input, &Config::default());
577-
output.is_some() && output.unwrap() == expected
626+
output.is_some() && output.unwrap().snippet == expected
578627
}
579628

580629
#[test]

src/macros.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ impl MacroBranch {
13231323
config.set().max_width(new_width);
13241324

13251325
// First try to format as items, then as statements.
1326-
let new_body = match ::format_snippet(&body_str, &config) {
1326+
let new_body_snippet = match ::format_snippet(&body_str, &config) {
13271327
Some(new_body) => new_body,
13281328
None => {
13291329
let new_width = new_width + config.tab_spaces();
@@ -1334,15 +1334,23 @@ impl MacroBranch {
13341334
}
13351335
}
13361336
};
1337-
let new_body = wrap_str(new_body, config.max_width(), shape)?;
1337+
let new_body = wrap_str(
1338+
new_body_snippet.snippet.to_string(),
1339+
config.max_width(),
1340+
shape,
1341+
)?;
13381342

13391343
// Indent the body since it is in a block.
13401344
let indent_str = body_indent.to_string(&config);
13411345
let mut new_body = LineClasses::new(new_body.trim_right())
1346+
.enumerate()
13421347
.fold(
13431348
(String::new(), true),
1344-
|(mut s, need_indent), (kind, ref l)| {
1345-
if !l.is_empty() && need_indent {
1349+
|(mut s, need_indent), (i, (kind, ref l))| {
1350+
if !l.is_empty()
1351+
&& need_indent
1352+
&& !new_body_snippet.is_line_non_formatted(i + 1)
1353+
{
13461354
s += &indent_str;
13471355
}
13481356
(s + l + "\n", !kind.is_string() || l.ends_with('\\'))

src/utils.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ use syntax::ptr;
2121
use syntax::source_map::{BytePos, Span, NO_EXPANSION};
2222

2323
use comment::{filter_normal_code, CharClasses, FullCodeCharKind};
24+
use config::Config;
2425
use rewrite::RewriteContext;
25-
use shape::Shape;
26+
use shape::{Indent, Shape};
2627

2728
pub const DEPR_SKIP_ANNOTATION: &str = "rustfmt_skip";
2829
pub const SKIP_ANNOTATION: &str = "rustfmt::skip";
@@ -482,6 +483,44 @@ pub fn remove_trailing_white_spaces(text: &str) -> String {
482483
buffer
483484
}
484485

486+
/// Trims a minimum of leading whitespaces so that the content layout is kept and aligns to indent.
487+
pub fn trim_left_preserve_layout(orig: &str, indent: &Indent, config: &Config) -> String {
488+
let prefix_whitespace_min = orig
489+
.lines()
490+
// skip the line with the starting sigil since the leading whitespace is removed
491+
// otherwise, the minimum would always be zero
492+
.skip(1)
493+
.filter(|line| !line.is_empty())
494+
.map(|line| {
495+
let mut width = 0;
496+
for c in line.chars() {
497+
match c {
498+
' ' => width += 1,
499+
'\t' => width += config.tab_spaces(),
500+
_ => break,
501+
}
502+
}
503+
width
504+
})
505+
.min()
506+
.unwrap_or(0);
507+
508+
let indent_str = indent.to_string(config);
509+
let mut lines = orig.lines();
510+
let first_line = lines.next().unwrap();
511+
let rest = lines
512+
.map(|line| {
513+
if line.is_empty() {
514+
String::from("\n")
515+
} else {
516+
format!("\n{}{}", indent_str, &line[prefix_whitespace_min..])
517+
}
518+
})
519+
.collect::<Vec<String>>()
520+
.concat();
521+
format!("{}{}", first_line, rest)
522+
}
523+
485524
#[test]
486525
fn test_remove_trailing_white_spaces() {
487526
let s = " r#\"\n test\n \"#";

0 commit comments

Comments
 (0)