From a5cb0e7780a4c02142f6a592bf29991802d3374c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 29 Jun 2026 17:51:57 +0200 Subject: [PATCH 1/2] Fix handling of inlining of `no_inline` of foreign items --- src/librustdoc/clean/inline.rs | 182 ++++++++++++------ src/librustdoc/passes/stripper.rs | 2 + .../auxiliary/inline-foreign-no_inline.rs | 9 + .../reexport/inline-foreign-no_inline.rs | 22 +++ 4 files changed, 161 insertions(+), 54 deletions(-) create mode 100644 tests/rustdoc-html/reexport/auxiliary/inline-foreign-no_inline.rs create mode 100644 tests/rustdoc-html/reexport/inline-foreign-no_inline.rs diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index 08478a62b7d85..9469c806a26ad 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -142,7 +142,7 @@ pub(crate) fn try_inline( Res::Def(DefKind::Ctor(..), _) | Res::SelfCtor(..) => return Some(Vec::new()), Res::Def(DefKind::Mod, did) => { record_extern_fqn(cx, did, ItemType::Module); - clean::ModuleItem(build_module(cx, did, visited)) + clean::ModuleItem(build_module(cx, did, name, visited)) } Res::Def(DefKind::Static { .. }, did) => { record_extern_fqn(cx, did, ItemType::Static); @@ -207,6 +207,7 @@ pub(crate) fn try_inline_glob( let mut items = build_module_items( cx, did, + cx.tcx.item_name(did), visited, inlined_names, Some(&reexports), @@ -665,16 +666,43 @@ pub(crate) fn build_impl( )); } -fn build_module(cx: &mut DocContext<'_>, did: DefId, visited: &mut DefIdSet) -> clean::Module { - let items = build_module_items(cx, did, visited, &mut FxHashSet::default(), None, None); +fn build_module( + cx: &mut DocContext<'_>, + did: DefId, + name: Symbol, + visited: &mut DefIdSet, +) -> clean::Module { + let items = build_module_items(cx, did, name, visited, &mut FxHashSet::default(), None, None); let span = clean::Span::new(cx.tcx.def_span(did)); clean::Module { items, span } } +// We are only interested into `Res::Def`. And in there, we only want "items" which get their own +// rustdoc page. So not `DefKind::Ctor` for example (which is returned by `tcx.module_children()`). +fn should_ignore_res(res: Res) -> bool { + !matches!( + res, + Res::Def(DefKind::Trait, _) + | Res::Def(DefKind::TraitAlias, _) + | Res::Def(DefKind::Fn, _) + | Res::Def(DefKind::Struct, _) + | Res::Def(DefKind::Union, _) + | Res::Def(DefKind::TyAlias, _) + | Res::Def(DefKind::Enum, _) + | Res::Def(DefKind::ForeignTy, _) + | Res::Def(DefKind::Variant, _) + | Res::Def(DefKind::Mod, _) + | Res::Def(DefKind::Static { .. }, _) + | Res::Def(DefKind::Const { .. }, _) + | Res::Def(DefKind::Macro(_), _) + ) +} + fn build_module_items( cx: &mut DocContext<'_>, - did: DefId, + module_def_id: DefId, + module_name: Symbol, visited: &mut DefIdSet, inlined_names: &mut FxHashSet<(ItemType, Symbol)>, allowed_def_ids: Option<&DefIdSet>, @@ -685,61 +713,107 @@ fn build_module_items( // If we're re-exporting a re-export it may actually re-export something in // two namespaces, so the target may be listed twice. Make sure we only // visit each node at most once. - for item in cx.tcx.module_children(did).iter() { - if item.vis.is_public() { - let res = item.res.expect_non_local(); - if let Some(def_id) = res.opt_def_id() - && let Some(allowed_def_ids) = allowed_def_ids - && !allowed_def_ids.contains(&def_id) + for item in cx.tcx.module_children(module_def_id).iter() { + if !item.vis.is_public() { + continue; + } + let res = item.res.expect_non_local(); + if let Some(def_id) = res.opt_def_id() + && let Some(allowed_def_ids) = allowed_def_ids + && !allowed_def_ids.contains(&def_id) + { + continue; + } + if let Some(def_id) = res.mod_def_id() { + // If we're inlining a glob import, it's possible to have + // two distinct modules with the same name. We don't want to + // inline it, or mark any of its contents as visited. + if module_def_id == def_id + || inlined_names.contains(&(ItemType::Module, item.ident.name)) + || !visited.insert(def_id) { continue; } - if let Some(def_id) = res.mod_def_id() { - // If we're inlining a glob import, it's possible to have - // two distinct modules with the same name. We don't want to - // inline it, or mark any of its contents as visited. - if did == def_id - || inlined_names.contains(&(ItemType::Module, item.ident.name)) - || !visited.insert(def_id) - { - continue; - } - } - if let Res::PrimTy(p) = res { - // Primitive types can't be inlined so generate an import instead. - let prim_ty = clean::PrimitiveType::from(p); - items.push(clean::Item { - inner: Box::new(clean::ItemInner { - name: None, - // We can use the item's `DefId` directly since the only information ever - // used from it is `DefId.krate`. - item_id: ItemId::DefId(did), - attrs: Default::default(), - stability: None, - kind: clean::ImportItem(clean::Import::new_simple( - item.ident.name, - clean::ImportSource { - path: clean::Path { - res, - segments: thin_vec![clean::PathSegment { - name: prim_ty.as_sym(), - args: clean::GenericArgs::AngleBracketed { - args: Default::default(), - constraints: ThinVec::new(), - }, - }], - }, - did: None, + } + if let Res::PrimTy(p) = res { + // Primitive types can't be inlined so generate an import instead. + let prim_ty = clean::PrimitiveType::from(p); + items.push(clean::Item { + inner: Box::new(clean::ItemInner { + name: None, + // We can use the item's `DefId` directly since the only information ever + // used from it is `DefId.krate`. + item_id: ItemId::DefId(module_def_id), + attrs: Default::default(), + stability: None, + kind: clean::ImportItem(clean::Import::new_simple( + item.ident.name, + clean::ImportSource { + path: clean::Path { + res, + segments: thin_vec![clean::PathSegment { + name: prim_ty.as_sym(), + args: clean::GenericArgs::AngleBracketed { + args: Default::default(), + constraints: ThinVec::new(), + }, + }], }, - true, - )), - cfg: None, - inline_stmt_id: None, - }), - }); - } else if let Some(i) = try_inline(cx, res, item.ident.name, attrs, visited) { - items.extend(i) + did: None, + }, + true, + )), + cfg: None, + inline_stmt_id: None, + }), + }); + } else if let Some(def_id) = res.opt_def_id() + && let Some(reexport) = item.reexport_chain.first() + && let Some(reexport_def_id) = reexport.id() + && find_attr!( + load_attrs(cx.tcx, reexport_def_id), + Doc(d) + if d.inline.first().is_some_and(|(inline, _)| *inline == hir::attrs::DocInline::NoInline) + ) + { + if should_ignore_res(res) { + continue; } + // This item is reexported as `no_inline` so it shouldn't be inlined. + let item = Item::from_def_id_and_parts( + module_def_id, + None, + clean::ImportItem(clean::Import::new_simple( + item.ident.name, + clean::ImportSource { + path: clean::Path { + res, + segments: thin_vec![ + clean::PathSegment { + name: module_name, + args: clean::GenericArgs::AngleBracketed { + args: Default::default(), + constraints: ThinVec::new(), + }, + }, + clean::PathSegment { + name: cx.tcx.item_name(def_id), + args: clean::GenericArgs::AngleBracketed { + args: Default::default(), + constraints: ThinVec::new(), + }, + }, + ], + }, + did: None, + }, + true, + )), + cx.tcx, + ); + items.push(item); + } else if let Some(i) = try_inline(cx, res, item.ident.name, attrs, visited) { + items.extend(i) } } diff --git a/src/librustdoc/passes/stripper.rs b/src/librustdoc/passes/stripper.rs index bf4e842ceec3f..4c6d62917ab24 100644 --- a/src/librustdoc/passes/stripper.rs +++ b/src/librustdoc/passes/stripper.rs @@ -287,12 +287,14 @@ impl DocFolder for ImportStripper<'_> { clean::ImportItem(imp) if !self.document_hidden && self.import_should_be_hidden(&i, imp) => { + debug!("ImportStripper: stripping {:?}", i.name); None } // clean::ImportItem(_) if !self.document_hidden && i.is_doc_hidden() => None, clean::ExternCrateItem { .. } | clean::ImportItem(..) if i.visibility(self.tcx) != Some(Visibility::Public) => { + debug!("ImportStripper: stripping {:?}", i.name); None } _ => Some(self.fold_item_recur(i)), diff --git a/tests/rustdoc-html/reexport/auxiliary/inline-foreign-no_inline.rs b/tests/rustdoc-html/reexport/auxiliary/inline-foreign-no_inline.rs new file mode 100644 index 0000000000000..e27ad8d7f4e80 --- /dev/null +++ b/tests/rustdoc-html/reexport/auxiliary/inline-foreign-no_inline.rs @@ -0,0 +1,9 @@ +#[doc(no_inline)] +pub use crate::{ + future::{Future, FutureExt as _}, +}; + +mod future { + pub struct Future; + pub trait FutureExt {} +} diff --git a/tests/rustdoc-html/reexport/inline-foreign-no_inline.rs b/tests/rustdoc-html/reexport/inline-foreign-no_inline.rs new file mode 100644 index 0000000000000..329a36cc9d264 --- /dev/null +++ b/tests/rustdoc-html/reexport/inline-foreign-no_inline.rs @@ -0,0 +1,22 @@ +// This test ensures that an inlined foreign item with `#[doc(no_inline)]` is +// not inlined. +// This is a regression test for . + +//@ aux-build: inline-foreign-no_inline.rs + +#![crate_name = "foo"] + +extern crate inline_foreign_no_inline; + +// Since we cannot inline `inline_foreign_no_inline` because it has `no_inline`, there +// should have no other items than "Module". +//@ has 'foo/index.html' +//@ count - '//*[@id="main-content"]/h2' 1 +//@ has - '//*[@id="main-content"]/h2' 'Module' +//@ has - '//*[@id="main-content"]/dl[@class="item-table"]/dt' 'dep' + +//@ has 'foo/dep/index.html' +//@ has - '//*[@class="item-table reexports"]/dt' 'pub use dep::Future;' +//@ has - '//*[@class="item-table reexports"]/dt' 'pub use dep::FutureExt as _;' +#[doc(inline)] +pub use inline_foreign_no_inline as dep; From 9aba0b60dddcb4eb3c1154974ed10279117f2a5c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 30 Jun 2026 12:55:28 +0200 Subject: [PATCH 2/2] Add rustdoc HTML test to ensure that the inlining of a `#[doc(no_inline)]` is working as expected --- .../rustdoc-html/reexport/inline-no_inline.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/rustdoc-html/reexport/inline-no_inline.rs diff --git a/tests/rustdoc-html/reexport/inline-no_inline.rs b/tests/rustdoc-html/reexport/inline-no_inline.rs new file mode 100644 index 0000000000000..af483181a487c --- /dev/null +++ b/tests/rustdoc-html/reexport/inline-no_inline.rs @@ -0,0 +1,21 @@ +// This test checks that inlining a `no_inline` is done correctly. + +#![crate_name = "foo"] + +//@ has 'foo/index.html' +// There should be `Re-exports` and `Structs` +//@ count - '//*[@id="main-content"]/h2' 2 +//@ has - '//*[@id="main-content"]/h2[@class="section-header"]' 'Re-exports' +//@ has - '//*[@id="main-content"]/h2[@class="section-header"]' 'Structs' + +//@ has - '//*[@id="main-content"]/dl[@class="item-table reexports"]/dt' 'pub use self::bar::A;' +//@ has - '//*[@id="main-content"]/dl[@class="item-table"]/dt' 'X' + +mod bar { + pub struct A; +} + +#[doc(no_inline)] +pub use self::bar::A; +#[doc(inline)] +pub use self::A as X;