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

Commit e731a5a

Browse files
authored
Rollup merge of rust-lang#78400 - GuillaumeGomez:fix-unindent, r=jyn514
Fix unindent in doc comments Fixes rust-lang#70732 r? ``@jyn514``
2 parents 0716724 + 87f2897 commit e731a5a

File tree

4 files changed

+163
-92
lines changed

4 files changed

+163
-92
lines changed
Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::cmp;
2-
use std::string::String;
32

4-
use crate::clean::{self, DocFragment, Item};
3+
use crate::clean::{self, DocFragment, DocFragmentKind, Item};
54
use crate::core::DocContext;
65
use crate::fold::{self, DocFolder};
76
use crate::passes::Pass;
@@ -35,65 +34,81 @@ impl clean::Attributes {
3534
}
3635

3736
fn unindent_fragments(docs: &mut Vec<DocFragment>) {
38-
for fragment in docs {
39-
fragment.doc = unindent(&fragment.doc);
40-
}
41-
}
42-
43-
fn unindent(s: &str) -> String {
44-
let lines = s.lines().collect::<Vec<&str>>();
45-
let mut saw_first_line = false;
46-
let mut saw_second_line = false;
47-
let min_indent = lines.iter().fold(usize::MAX, |min_indent, line| {
48-
// After we see the first non-whitespace line, look at
49-
// the line we have. If it is not whitespace, and therefore
50-
// part of the first paragraph, then ignore the indentation
51-
// level of the first line
52-
let ignore_previous_indents =
53-
saw_first_line && !saw_second_line && !line.chars().all(|c| c.is_whitespace());
37+
// `add` is used in case the most common sugared doc syntax is used ("/// "). The other
38+
// fragments kind's lines are never starting with a whitespace unless they are using some
39+
// markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
40+
// we need to take into account the fact that the minimum indent minus one (to take this
41+
// whitespace into account).
42+
//
43+
// For example:
44+
//
45+
// /// hello!
46+
// #[doc = "another"]
47+
//
48+
// In this case, you want "hello! another" and not "hello! another".
49+
let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
50+
&& docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
51+
{
52+
// In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
53+
// "decide" how much the minimum indent will be.
54+
1
55+
} else {
56+
0
57+
};
5458

55-
let min_indent = if ignore_previous_indents { usize::MAX } else { min_indent };
59+
// `min_indent` is used to know how much whitespaces from the start of each lines must be
60+
// removed. Example:
61+
//
62+
// /// hello!
63+
// #[doc = "another"]
64+
//
65+
// In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
66+
// 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
67+
// (5 - 1) whitespaces.
68+
let min_indent = match docs
69+
.iter()
70+
.map(|fragment| {
71+
fragment.doc.lines().fold(usize::MAX, |min_indent, line| {
72+
if line.chars().all(|c| c.is_whitespace()) {
73+
min_indent
74+
} else {
75+
// Compare against either space or tab, ignoring whether they are
76+
// mixed or not.
77+
let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
78+
cmp::min(min_indent, whitespace)
79+
+ if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }
80+
}
81+
})
82+
})
83+
.min()
84+
{
85+
Some(x) => x,
86+
None => return,
87+
};
5688

57-
if saw_first_line {
58-
saw_second_line = true;
89+
for fragment in docs {
90+
if fragment.doc.lines().count() == 0 {
91+
continue;
5992
}
6093

61-
if line.chars().all(|c| c.is_whitespace()) {
62-
min_indent
94+
let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
95+
min_indent - add
6396
} else {
64-
saw_first_line = true;
65-
let mut whitespace = 0;
66-
line.chars().all(|char| {
67-
// Compare against either space or tab, ignoring whether they
68-
// are mixed or not
69-
if char == ' ' || char == '\t' {
70-
whitespace += 1;
71-
true
97+
min_indent
98+
};
99+
100+
fragment.doc = fragment
101+
.doc
102+
.lines()
103+
.map(|line| {
104+
if line.chars().all(|c| c.is_whitespace()) {
105+
line.to_string()
72106
} else {
73-
false
107+
assert!(line.len() >= min_indent);
108+
line[min_indent..].to_string()
74109
}
75-
});
76-
cmp::min(min_indent, whitespace)
77-
}
78-
});
79-
80-
if !lines.is_empty() {
81-
let mut unindented = vec![lines[0].trim_start().to_string()];
82-
unindented.extend_from_slice(
83-
&lines[1..]
84-
.iter()
85-
.map(|&line| {
86-
if line.chars().all(|c| c.is_whitespace()) {
87-
line.to_string()
88-
} else {
89-
assert!(line.len() >= min_indent);
90-
line[min_indent..].to_string()
91-
}
92-
})
93-
.collect::<Vec<_>>(),
94-
);
95-
unindented.join("\n")
96-
} else {
97-
s.to_string()
110+
})
111+
.collect::<Vec<_>>()
112+
.join("\n");
98113
}
99114
}
Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,63 @@
11
use super::*;
2+
use rustc_span::source_map::DUMMY_SP;
3+
4+
fn create_doc_fragment(s: &str) -> Vec<DocFragment> {
5+
vec![DocFragment {
6+
line: 0,
7+
span: DUMMY_SP,
8+
parent_module: None,
9+
doc: s.to_string(),
10+
kind: DocFragmentKind::SugaredDoc,
11+
}]
12+
}
13+
14+
#[track_caller]
15+
fn run_test(input: &str, expected: &str) {
16+
let mut s = create_doc_fragment(input);
17+
unindent_fragments(&mut s);
18+
assert_eq!(s[0].doc, expected);
19+
}
220

321
#[test]
422
fn should_unindent() {
5-
let s = " line1\n line2".to_string();
6-
let r = unindent(&s);
7-
assert_eq!(r, "line1\nline2");
23+
run_test(" line1\n line2", "line1\nline2");
824
}
925

1026
#[test]
1127
fn should_unindent_multiple_paragraphs() {
12-
let s = " line1\n\n line2".to_string();
13-
let r = unindent(&s);
14-
assert_eq!(r, "line1\n\nline2");
28+
run_test(" line1\n\n line2", "line1\n\nline2");
1529
}
1630

1731
#[test]
1832
fn should_leave_multiple_indent_levels() {
1933
// Line 2 is indented another level beyond the
2034
// base indentation and should be preserved
21-
let s = " line1\n\n line2".to_string();
22-
let r = unindent(&s);
23-
assert_eq!(r, "line1\n\n line2");
35+
run_test(" line1\n\n line2", "line1\n\n line2");
2436
}
2537

2638
#[test]
2739
fn should_ignore_first_line_indent() {
28-
// The first line of the first paragraph may not be indented as
29-
// far due to the way the doc string was written:
30-
//
31-
// #[doc = "Start way over here
32-
// and continue here"]
33-
let s = "line1\n line2".to_string();
34-
let r = unindent(&s);
35-
assert_eq!(r, "line1\nline2");
40+
run_test("line1\n line2", "line1\n line2");
3641
}
3742

3843
#[test]
3944
fn should_not_ignore_first_line_indent_in_a_single_line_para() {
40-
let s = "line1\n\n line2".to_string();
41-
let r = unindent(&s);
42-
assert_eq!(r, "line1\n\n line2");
45+
run_test("line1\n\n line2", "line1\n\n line2");
4346
}
4447

4548
#[test]
4649
fn should_unindent_tabs() {
47-
let s = "\tline1\n\tline2".to_string();
48-
let r = unindent(&s);
49-
assert_eq!(r, "line1\nline2");
50+
run_test("\tline1\n\tline2", "line1\nline2");
5051
}
5152

5253
#[test]
5354
fn should_trim_mixed_indentation() {
54-
let s = "\t line1\n\t line2".to_string();
55-
let r = unindent(&s);
56-
assert_eq!(r, "line1\nline2");
57-
58-
let s = " \tline1\n \tline2".to_string();
59-
let r = unindent(&s);
60-
assert_eq!(r, "line1\nline2");
55+
run_test("\t line1\n\t line2", "line1\nline2");
56+
run_test(" \tline1\n \tline2", "line1\nline2");
6157
}
6258

6359
#[test]
6460
fn should_not_trim() {
65-
let s = "\t line1 \n\t line2".to_string();
66-
let r = unindent(&s);
67-
assert_eq!(r, "line1 \nline2");
68-
69-
let s = " \tline1 \n \tline2".to_string();
70-
let r = unindent(&s);
71-
assert_eq!(r, "line1 \nline2");
61+
run_test("\t line1 \n\t line2", "line1 \nline2");
62+
run_test(" \tline1 \n \tline2", "line1 \nline2");
7263
}

src/test/rustdoc/unindent.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Just some text.

src/test/rustdoc/unindent.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#![feature(external_doc)]
2+
3+
#![crate_name = "foo"]
4+
5+
// @has foo/struct.Example.html
6+
// @matches - '//pre[@class="rust rust-example-rendered"]' \
7+
// '(?m)let example = Example::new\(\)\n \.first\(\)\n \.second\(\)\n \.build\(\);\Z'
8+
/// ```rust
9+
/// let example = Example::new()
10+
/// .first()
11+
#[cfg_attr(not(feature = "one"), doc = " .second()")]
12+
/// .build();
13+
/// ```
14+
pub struct Example;
15+
16+
// @has foo/struct.F.html
17+
// @matches - '//pre[@class="rust rust-example-rendered"]' \
18+
// '(?m)let example = Example::new\(\)\n \.first\(\)\n \.another\(\)\n \.build\(\);\Z'
19+
///```rust
20+
///let example = Example::new()
21+
/// .first()
22+
#[cfg_attr(not(feature = "one"), doc = " .another()")]
23+
/// .build();
24+
/// ```
25+
pub struct F;
26+
27+
// @has foo/struct.G.html
28+
// @matches - '//pre[@class="rust rust-example-rendered"]' \
29+
// '(?m)let example = Example::new\(\)\n\.first\(\)\n \.another\(\)\n\.build\(\);\Z'
30+
///```rust
31+
///let example = Example::new()
32+
///.first()
33+
#[cfg_attr(not(feature = "one"), doc = " .another()")]
34+
///.build();
35+
///```
36+
pub struct G;
37+
38+
// @has foo/struct.H.html
39+
// @has - '//div[@class="docblock"]/p' 'no whitespace lol'
40+
///no whitespace
41+
#[doc = " lol"]
42+
pub struct H;
43+
44+
// @has foo/struct.I.html
45+
// @matches - '//pre[@class="rust rust-example-rendered"]' '(?m)4 whitespaces!\Z'
46+
/// 4 whitespaces!
47+
#[doc = "something"]
48+
pub struct I;
49+
50+
// @has foo/struct.J.html
51+
// @matches - '//div[@class="docblock"]/p' '(?m)a\nno whitespace\nJust some text.\Z'
52+
///a
53+
///no whitespace
54+
#[doc(include = "unindent.md")]
55+
pub struct J;
56+
57+
// @has foo/struct.K.html
58+
// @matches - '//pre[@class="rust rust-example-rendered"]' '(?m)4 whitespaces!\Z'
59+
///a
60+
///
61+
/// 4 whitespaces!
62+
///
63+
#[doc(include = "unindent.md")]
64+
pub struct K;

0 commit comments

Comments
 (0)