Skip to content

Commit 0412898

Browse files
authored
Rollup merge of #108057 - GuillaumeGomez:fix-reexport-attr-merge, r=notriddle
Prevent some attributes from being merged with others on reexports Final fix for #59368. As discussed on zulip [here](https://rust-lang.zulipchat.com/#narrow/stream/182449-t-compiler.2Fhelp/topic/Filtering.20sub.20attributes.20in.20ast.3A.3AAttribute), we need to clone the `Attribute` to be able to filter some parts of it. Then we need to go through the attributes to able to only keep what we want (everything except a few attributes in short). As for the second commit, when I wrote the test, I realized that the code to traverse all reexports one by one to collect all their attributes was not completely working so I fixed the few issues remaining. r? `@notriddle`
2 parents f65c6e4 + 374f798 commit 0412898

File tree

2 files changed

+178
-27
lines changed

2 files changed

+178
-27
lines changed

src/librustdoc/clean/mod.rs

Lines changed: 145 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub(crate) mod types;
1111
pub(crate) mod utils;
1212

1313
use rustc_ast as ast;
14+
use rustc_ast::token::{Token, TokenKind};
15+
use rustc_ast::tokenstream::{TokenStream, TokenTree};
1416
use rustc_attr as attr;
1517
use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet, IndexEntry};
1618
use rustc_hir as hir;
@@ -2079,8 +2081,8 @@ impl<'hir> hir::intravisit::Visitor<'hir> for OneLevelVisitor<'hir> {
20792081
fn visit_item(&mut self, item: &'hir hir::Item<'hir>) {
20802082
if self.item.is_none()
20812083
&& item.ident == self.looking_for
2082-
&& matches!(item.kind, hir::ItemKind::Use(_, _))
2083-
|| item.owner_id.def_id == self.target_def_id
2084+
&& (matches!(item.kind, hir::ItemKind::Use(_, _))
2085+
|| item.owner_id.def_id == self.target_def_id)
20842086
{
20852087
self.item = Some(item);
20862088
}
@@ -2096,34 +2098,149 @@ fn get_all_import_attributes<'hir>(
20962098
tcx: TyCtxt<'hir>,
20972099
target_def_id: LocalDefId,
20982100
attributes: &mut Vec<ast::Attribute>,
2101+
is_inline: bool,
20992102
) {
21002103
let hir_map = tcx.hir();
21012104
let mut visitor = OneLevelVisitor::new(hir_map, target_def_id);
21022105
let mut visited = FxHashSet::default();
21032106
// If the item is an import and has at least a path with two parts, we go into it.
2104-
while let hir::ItemKind::Use(path, _) = item.kind &&
2105-
path.segments.len() > 1 &&
2106-
let hir::def::Res::Def(_, def_id) = path.segments[path.segments.len() - 2].res &&
2107-
visited.insert(def_id)
2108-
{
2109-
if let Some(hir::Node::Item(parent_item)) = hir_map.get_if_local(def_id) {
2110-
// We add the attributes from this import into the list.
2111-
attributes.extend_from_slice(hir_map.attrs(item.hir_id()));
2112-
// We get the `Ident` we will be looking for into `item`.
2113-
let looking_for = path.segments[path.segments.len() - 1].ident;
2114-
visitor.reset(looking_for);
2115-
hir::intravisit::walk_item(&mut visitor, parent_item);
2116-
if let Some(i) = visitor.item {
2117-
item = i;
2118-
} else {
2119-
break;
2107+
while let hir::ItemKind::Use(path, _) = item.kind && visited.insert(item.hir_id()) {
2108+
// We add the attributes from this import into the list.
2109+
add_without_unwanted_attributes(attributes, hir_map.attrs(item.hir_id()), is_inline);
2110+
2111+
let def_id = if path.segments.len() > 1 {
2112+
match path.segments[path.segments.len() - 2].res {
2113+
hir::def::Res::Def(_, def_id) => def_id,
2114+
_ => break,
2115+
}
2116+
} else {
2117+
// If the path doesn't have a parent, then the parent is the current module.
2118+
tcx.parent(item.owner_id.def_id.to_def_id())
2119+
};
2120+
2121+
let Some(parent) = hir_map.get_if_local(def_id) else { break };
2122+
2123+
// We get the `Ident` we will be looking for into `item`.
2124+
let looking_for = path.segments[path.segments.len() - 1].ident;
2125+
visitor.reset(looking_for);
2126+
2127+
match parent {
2128+
hir::Node::Item(parent_item) => {
2129+
hir::intravisit::walk_item(&mut visitor, parent_item);
2130+
}
2131+
hir::Node::Crate(m) => {
2132+
hir::intravisit::walk_mod(
2133+
&mut visitor,
2134+
m,
2135+
tcx.local_def_id_to_hir_id(def_id.as_local().unwrap()),
2136+
);
21202137
}
2138+
_ => break,
2139+
}
2140+
if let Some(i) = visitor.item {
2141+
item = i;
21212142
} else {
21222143
break;
21232144
}
21242145
}
21252146
}
21262147

2148+
fn filter_tokens_from_list(
2149+
args_tokens: TokenStream,
2150+
should_retain: impl Fn(&TokenTree) -> bool,
2151+
) -> Vec<TokenTree> {
2152+
let mut tokens = Vec::with_capacity(args_tokens.len());
2153+
let mut skip_next_comma = false;
2154+
for token in args_tokens.into_trees() {
2155+
match token {
2156+
TokenTree::Token(Token { kind: TokenKind::Comma, .. }, _) if skip_next_comma => {
2157+
skip_next_comma = false;
2158+
}
2159+
token if should_retain(&token) => {
2160+
skip_next_comma = false;
2161+
tokens.push(token);
2162+
}
2163+
_ => {
2164+
skip_next_comma = true;
2165+
}
2166+
}
2167+
}
2168+
tokens
2169+
}
2170+
2171+
/// When inlining items, we merge its attributes (and all the reexports attributes too) with the
2172+
/// final reexport. For example:
2173+
///
2174+
/// ```ignore (just an example)
2175+
/// #[doc(hidden, cfg(feature = "foo"))]
2176+
/// pub struct Foo;
2177+
///
2178+
/// #[doc(cfg(feature = "bar"))]
2179+
/// #[doc(hidden, no_inline)]
2180+
/// pub use Foo as Foo1;
2181+
///
2182+
/// #[doc(inline)]
2183+
/// pub use Foo2 as Bar;
2184+
/// ```
2185+
///
2186+
/// So `Bar` at the end will have both `cfg(feature = "...")`. However, we don't want to merge all
2187+
/// attributes so we filter out the following ones:
2188+
/// * `doc(inline)`
2189+
/// * `doc(no_inline)`
2190+
/// * `doc(hidden)`
2191+
fn add_without_unwanted_attributes(
2192+
attrs: &mut Vec<ast::Attribute>,
2193+
new_attrs: &[ast::Attribute],
2194+
is_inline: bool,
2195+
) {
2196+
// If it's `#[doc(inline)]`, we don't want all attributes, otherwise we keep everything.
2197+
if !is_inline {
2198+
attrs.extend_from_slice(new_attrs);
2199+
return;
2200+
}
2201+
for attr in new_attrs {
2202+
let mut attr = attr.clone();
2203+
match attr.kind {
2204+
ast::AttrKind::Normal(ref mut normal) => {
2205+
if let [ident] = &*normal.item.path.segments &&
2206+
let ident = ident.ident.name &&
2207+
ident == sym::doc
2208+
{
2209+
match normal.item.args {
2210+
ast::AttrArgs::Delimited(ref mut args) => {
2211+
let tokens =
2212+
filter_tokens_from_list(args.tokens.clone(), |token| {
2213+
!matches!(
2214+
token,
2215+
TokenTree::Token(
2216+
Token {
2217+
kind: TokenKind::Ident(
2218+
sym::hidden | sym::inline | sym::no_inline,
2219+
_,
2220+
),
2221+
..
2222+
},
2223+
_,
2224+
),
2225+
)
2226+
});
2227+
args.tokens = TokenStream::new(tokens);
2228+
attrs.push(attr);
2229+
}
2230+
ast::AttrArgs::Empty | ast::AttrArgs::Eq(..) => {
2231+
attrs.push(attr);
2232+
continue;
2233+
}
2234+
}
2235+
}
2236+
}
2237+
ast::AttrKind::DocComment(..) => {
2238+
attrs.push(attr);
2239+
}
2240+
}
2241+
}
2242+
}
2243+
21272244
fn clean_maybe_renamed_item<'tcx>(
21282245
cx: &mut DocContext<'tcx>,
21292246
item: &hir::Item<'tcx>,
@@ -2212,19 +2329,20 @@ fn clean_maybe_renamed_item<'tcx>(
22122329
{
22132330
// First, we add the attributes from the current import.
22142331
extra_attrs.extend_from_slice(inline::load_attrs(cx, import_id.to_def_id()));
2332+
let is_inline = extra_attrs.lists(sym::doc).get_word_attr(sym::inline).is_some();
22152333
// Then we get all the various imports' attributes.
2216-
get_all_import_attributes(use_node, cx.tcx, item.owner_id.def_id, &mut extra_attrs);
2334+
get_all_import_attributes(use_node, cx.tcx, item.owner_id.def_id, &mut extra_attrs, is_inline);
2335+
add_without_unwanted_attributes(&mut extra_attrs, inline::load_attrs(cx, def_id), is_inline);
2336+
} else {
2337+
// We only keep the item's attributes.
2338+
extra_attrs.extend_from_slice(inline::load_attrs(cx, def_id));
22172339
}
22182340

2219-
let mut item = if !extra_attrs.is_empty() {
2220-
extra_attrs.extend_from_slice(inline::load_attrs(cx, def_id));
2221-
let attrs = Attributes::from_ast(&extra_attrs);
2222-
let cfg = extra_attrs.cfg(cx.tcx, &cx.cache.hidden_cfg);
2341+
let attrs = Attributes::from_ast(&extra_attrs);
2342+
let cfg = extra_attrs.cfg(cx.tcx, &cx.cache.hidden_cfg);
22232343

2224-
Item::from_def_id_and_attrs_and_parts(def_id, Some(name), kind, Box::new(attrs), cfg)
2225-
} else {
2226-
Item::from_def_id_and_parts(def_id, Some(name), kind, cx)
2227-
};
2344+
let mut item =
2345+
Item::from_def_id_and_attrs_and_parts(def_id, Some(name), kind, Box::new(attrs), cfg);
22282346
item.inline_stmt_id = import_id.map(|def_id| def_id.to_def_id());
22292347
vec![item]
22302348
})

tests/rustdoc/reexport-attr-merge.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Regression test for <https://github.com/rust-lang/rust/issues/59368>.
2+
// The goal is to ensure that `doc(hidden)`, `doc(inline)` and `doc(no_inline)`
3+
// are not copied from an item when inlined.
4+
5+
#![crate_name = "foo"]
6+
#![feature(doc_cfg)]
7+
8+
// @has 'foo/index.html'
9+
10+
#[doc(hidden, cfg(feature = "foo"))]
11+
pub struct Foo;
12+
13+
#[doc(hidden, no_inline, cfg(feature = "bar"))]
14+
pub use Foo as Foo1;
15+
16+
#[doc(hidden, inline)]
17+
pub use Foo1 as Foo2;
18+
19+
// First we ensure that only the reexport `Bar2` and the inlined struct `Bar`
20+
// are inlined.
21+
// @count - '//a[@class="struct"]' 2
22+
// Then we check that both `cfg` are displayed.
23+
// @has - '//*[@class="stab portability"]' 'foo'
24+
// @has - '//*[@class="stab portability"]' 'bar'
25+
// And finally we check that the only element displayed is `Bar`.
26+
// @has - '//a[@class="struct"]' 'Bar'
27+
#[doc(inline)]
28+
pub use Foo2 as Bar;
29+
30+
// This one should appear but `Bar2` won't be linked because there is no
31+
// `#[doc(inline)]`.
32+
// @has - '//*[@id="reexport.Bar2"]' 'pub use Foo2 as Bar2;'
33+
pub use Foo2 as Bar2;

0 commit comments

Comments
 (0)