Skip to content

Commit 8b23930

Browse files
committed
Auto merge of #96282 - petrochenkov:unindent, r=GuillaumeGomez
rustdoc: Unindent doc fragments on `Attributes` construction `Attributes` can be constructed at arbitrary points, even after the `unindent_comments` pass. `Attributes` that are constructed too late end up unindented. All doc fragments need to be eventually indented before use, so there are no reasons to not do this immediately during their construction. Fixes https://rust-lang.zulipchat.com/#narrow/stream/266220-rustdoc/topic/.60unindent_comments.60.20cannot.20work.20as.20a.20separate.20pass. I'm not sure how to make a minimized reproduction, but unindenting the fragments during their construction should fix the issue.. by construction, and I also verified that all doc strings now hit the `resolver_caches.markdown_links` cache in #94857.
2 parents 0b3404b + 7803a41 commit 8b23930

File tree

7 files changed

+89
-133
lines changed

7 files changed

+89
-133
lines changed

src/librustdoc/clean/types.rs

+86-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
use std::cell::RefCell;
22
use std::default::Default;
3-
use std::fmt;
43
use std::hash::Hash;
5-
use std::iter;
64
use std::lazy::SyncOnceCell as OnceCell;
75
use std::path::PathBuf;
86
use std::rc::Rc;
97
use std::sync::Arc;
10-
use std::vec;
8+
use std::{cmp, fmt, iter};
119

1210
use arrayvec::ArrayVec;
1311

@@ -55,6 +53,9 @@ crate use self::Type::{
5553
};
5654
crate use self::Visibility::{Inherited, Public};
5755

56+
#[cfg(test)]
57+
mod tests;
58+
5859
crate type ItemIdSet = FxHashSet<ItemId>;
5960

6061
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
@@ -1028,6 +1029,86 @@ crate fn collapse_doc_fragments(doc_strings: &[DocFragment]) -> String {
10281029
acc
10291030
}
10301031

1032+
/// Removes excess indentation on comments in order for the Markdown
1033+
/// to be parsed correctly. This is necessary because the convention for
1034+
/// writing documentation is to provide a space between the /// or //! marker
1035+
/// and the doc text, but Markdown is whitespace-sensitive. For example,
1036+
/// a block of text with four-space indentation is parsed as a code block,
1037+
/// so if we didn't unindent comments, these list items
1038+
///
1039+
/// /// A list:
1040+
/// ///
1041+
/// /// - Foo
1042+
/// /// - Bar
1043+
///
1044+
/// would be parsed as if they were in a code block, which is likely not what the user intended.
1045+
fn unindent_doc_fragments(docs: &mut Vec<DocFragment>) {
1046+
// `add` is used in case the most common sugared doc syntax is used ("/// "). The other
1047+
// fragments kind's lines are never starting with a whitespace unless they are using some
1048+
// markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
1049+
// we need to take into account the fact that the minimum indent minus one (to take this
1050+
// whitespace into account).
1051+
//
1052+
// For example:
1053+
//
1054+
// /// hello!
1055+
// #[doc = "another"]
1056+
//
1057+
// In this case, you want "hello! another" and not "hello! another".
1058+
let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
1059+
&& docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
1060+
{
1061+
// In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
1062+
// "decide" how much the minimum indent will be.
1063+
1
1064+
} else {
1065+
0
1066+
};
1067+
1068+
// `min_indent` is used to know how much whitespaces from the start of each lines must be
1069+
// removed. Example:
1070+
//
1071+
// /// hello!
1072+
// #[doc = "another"]
1073+
//
1074+
// In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
1075+
// 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
1076+
// (5 - 1) whitespaces.
1077+
let Some(min_indent) = docs
1078+
.iter()
1079+
.map(|fragment| {
1080+
fragment.doc.as_str().lines().fold(usize::MAX, |min_indent, line| {
1081+
if line.chars().all(|c| c.is_whitespace()) {
1082+
min_indent
1083+
} else {
1084+
// Compare against either space or tab, ignoring whether they are
1085+
// mixed or not.
1086+
let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
1087+
cmp::min(min_indent, whitespace)
1088+
+ if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }
1089+
}
1090+
})
1091+
})
1092+
.min()
1093+
else {
1094+
return;
1095+
};
1096+
1097+
for fragment in docs {
1098+
if fragment.doc == kw::Empty {
1099+
continue;
1100+
}
1101+
1102+
let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
1103+
min_indent - add
1104+
} else {
1105+
min_indent
1106+
};
1107+
1108+
fragment.indent = min_indent;
1109+
}
1110+
}
1111+
10311112
/// A link that has not yet been rendered.
10321113
///
10331114
/// This link will be turned into a rendered link by [`Item::links`].
@@ -1119,6 +1200,8 @@ impl Attributes {
11191200
}
11201201
}
11211202

1203+
unindent_doc_fragments(&mut doc_strings);
1204+
11221205
Attributes { doc_strings, other_attrs }
11231206
}
11241207

src/librustdoc/passes/unindent_comments/tests.rs renamed to src/librustdoc/clean/types/tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fn create_doc_fragment(s: &str) -> Vec<DocFragment> {
2020
fn run_test(input: &str, expected: &str) {
2121
create_default_session_globals_then(|| {
2222
let mut s = create_doc_fragment(input);
23-
unindent_fragments(&mut s);
23+
unindent_doc_fragments(&mut s);
2424
assert_eq!(collapse_doc_fragments(&s), expected);
2525
});
2626
}

src/librustdoc/doctest.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1174,8 +1174,6 @@ impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> {
11741174
nested: F,
11751175
) {
11761176
let ast_attrs = self.tcx.hir().attrs(hir_id);
1177-
let mut attrs = Attributes::from_ast(ast_attrs, None);
1178-
11791177
if let Some(ref cfg) = ast_attrs.cfg(self.tcx, &FxHashSet::default()) {
11801178
if !cfg.matches(&self.sess.parse_sess, Some(self.sess.features_untracked())) {
11811179
return;
@@ -1187,9 +1185,9 @@ impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> {
11871185
self.collector.names.push(name);
11881186
}
11891187

1190-
attrs.unindent_doc_comments();
11911188
// The collapse-docs pass won't combine sugared/raw doc attributes, or included files with
11921189
// anything else, this will combine them for us.
1190+
let attrs = Attributes::from_ast(ast_attrs, None);
11931191
if let Some(doc) = attrs.collapsed_doc_value() {
11941192
// Use the outermost invocation, so that doctest names come from where the docs were written.
11951193
let span = ast_attrs

src/librustdoc/passes/collect_intra_doc_links/early.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ crate fn early_resolve_intra_doc_links(
6666
}
6767

6868
fn doc_attrs<'a>(attrs: impl Iterator<Item = &'a ast::Attribute>) -> Attributes {
69-
let mut attrs = Attributes::from_ast_iter(attrs.map(|attr| (attr, None)), true);
70-
attrs.unindent_doc_comments();
71-
attrs
69+
Attributes::from_ast_iter(attrs.map(|attr| (attr, None)), true)
7270
}
7371

7472
struct EarlyDocLinkResolver<'r, 'ra> {

src/librustdoc/passes/mod.rs

-5
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ crate use self::strip_private::STRIP_PRIVATE;
2424
mod strip_priv_imports;
2525
crate use self::strip_priv_imports::STRIP_PRIV_IMPORTS;
2626

27-
mod unindent_comments;
28-
crate use self::unindent_comments::UNINDENT_COMMENTS;
29-
3027
mod propagate_doc_cfg;
3128
crate use self::propagate_doc_cfg::PROPAGATE_DOC_CFG;
3229

@@ -81,7 +78,6 @@ crate enum Condition {
8178
crate const PASSES: &[Pass] = &[
8279
CHECK_DOC_TEST_VISIBILITY,
8380
STRIP_HIDDEN,
84-
UNINDENT_COMMENTS,
8581
STRIP_PRIVATE,
8682
STRIP_PRIV_IMPORTS,
8783
PROPAGATE_DOC_CFG,
@@ -96,7 +92,6 @@ crate const PASSES: &[Pass] = &[
9692
/// The list of passes run by default.
9793
crate const DEFAULT_PASSES: &[ConditionalPass] = &[
9894
ConditionalPass::always(COLLECT_TRAIT_IMPLS),
99-
ConditionalPass::always(UNINDENT_COMMENTS),
10095
ConditionalPass::always(CHECK_DOC_TEST_VISIBILITY),
10196
ConditionalPass::new(STRIP_HIDDEN, WhenNotDocumentHidden),
10297
ConditionalPass::new(STRIP_PRIVATE, WhenNotDocumentPrivate),

src/librustdoc/passes/unindent_comments.rs

-116
This file was deleted.

src/test/rustdoc-ui/issue-91713.stdout

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
Available passes for running rustdoc:
22
check_doc_test_visibility - run various visibility-related lints on doctests
33
strip-hidden - strips all `#[doc(hidden)]` items from the output
4-
unindent-comments - removes excess indentation on comments in order for markdown to like it
54
strip-private - strips all private items from a crate which cannot be seen externally, implies strip-priv-imports
65
strip-priv-imports - strips all private import statements (`use`, `extern crate`) from a crate
76
propagate-doc-cfg - propagates `#[doc(cfg(...))]` to child items
@@ -14,7 +13,6 @@ check-invalid-html-tags - detects invalid HTML tags in doc comments
1413

1514
Default passes for rustdoc:
1615
collect-trait-impls
17-
unindent-comments
1816
check_doc_test_visibility
1917
strip-hidden (when not --document-hidden-items)
2018
strip-private (when not --document-private-items)

0 commit comments

Comments
 (0)