Change:
From Given the following Rust code with in-line annotations of the expected behaviour: On: does not seem to be able to generate feature requirement labels on types in private modules () that are publicly reexported (). The labels do not show up on the module overview nor page, but do propagate into items like functions for the given type. This renders the type without any feature requirement, neither in the module overview: ! Nor on the page: ! Note that the label is propagated onto (does not have an explicit ), at least understood that! It is supposed to render the feature requirement for like , both in the module overview above as on the page: ! It seems tricky to combine feature requirements on the and reexport. In the case above they are the same, but what if: The type is only publicly available when and are specified. Specifying is fine but makes the contents of unreachable through the current module, specifying only should result in a "module not found" error. CC
This also affects re-exports of entire crates. Unfortunately there the fix is not clear at all, since the "parent" crate is obviously not guaranteed to have the same features as the crate being re-exported.
Within the same crate as well; it's not about the features that are enabled (though it is relevant which are available) but about the combined restrictions that are placed on the definition and reexport. Simplest I can think of is a union of all features (but gets complicated quick when inversions and vs is used) and later perhaps simplifying based on feature relations (if implies there is no need to display as requirement, if either definition or reexport requires ). That's necessary to cleanly solve crate reexports, since you effectively want to ensure that the top-level crate feature implies feature and hide it. If the whole resolver is made smart enough (perhaps this already exists, I'm not too familiar with Cargo) it could even throw warnings when there exist feature combinations that would lead to a compiler error (ie. explicitly reexporting a symbol that is not available for a certain set of features).
I wonder if this is just an ordering issue of the passes: The private modules have disappeared by the point where we pass all the data down so we don't see them.
Thanks for the suggestion - I moved before and (I've never built rustc/rustdoc from source, pleasantly surprised that only took just under 7min on a ThreadRipper :partyingface:) but it doesn't change anything to the generated output unfortunately :(
I observe this on public items as well: Does not show the badge at re-exports.
So from the following code: is stripped and then inlined. The stripped version contains all the correct information but the inlined one doesn't. Checking what's the best course here: either cloning the stripped item or propagating when inlining.

Discussion:
} /// `parent_module` refers to the parent of the re-export, not the original item <del> fn merge_attrs( </del> <ins> pub(crate) fn merge_attrs( </ins> cx: &mut DocContext<'_>, parent_module: Option<DefId>, old_attrs: Attrs<'_>,

cx: &mut DocContext<'_>, cfg: Option<Arc<Cfg>>, ) -> Item { <del> trace!("name={:?}, def_id={:?}", name, def_id); </del> <ins> trace!("name={:?}, def_id={:?} cfg={:?}", name, def_id, cfg); </ins> // Primitives and Keywords are written in the source code as private modules. // The modules need to be private so that nobody actually uses them, but the

| KeywordItem => [].iter(), } } <ins> /// Returns `true` if this item does not appear inside an impl block. pub(crate) fn is_non_assoc(&self) -> bool { matches!( self, StructItem(_) | UnionItem(_) | EnumItem(_) | TraitItem(_) | ModuleItem(_) | ExternCrateItem { .. } | FunctionItem(_) | TypedefItem(_) | OpaqueTyItem(_) | StaticItem(_) | ConstantItem(_) | TraitAliasItem(_) | ForeignFunctionItem(_) | ForeignStaticItem(_) | ForeignTypeItem | MacroItem(_) | ProcMacroItem(_) | PrimitiveItem(_) ) } </ins> } #[derive(Clone, Debug)]

(cfg, _) => cfg.as_deref().cloned(), }; <del> debug!("Portability {:?} - {:?} = {:?}", item.cfg, parent.and_then(|p| p.cfg.as_ref()), cfg); </del> <ins> debug!( "Portability {:?} {:?} (parent: {:?}) - {:?} = {:?}", item.name, item.cfg, parent, parent.and_then(|p| p.cfg.as_ref()), cfg ); </ins> Some(format!("<div class="stab portability">{}</div>", cfg?.render_long_html())) }

(cfg, _) => cfg.as_deref().cloned(), }; <del> debug!("Portability {:?} - {:?} = {:?}", item.cfg, parent.cfg, cfg); </del> <ins> debug!("Portability name={:?} {:?} - {:?} = {:?}", item.name, item.cfg, parent.cfg, cfg); </ins> if let Some(ref cfg) = cfg { tags += &tag_html("portability", &cfg.render_long_plain(), &cfg.render_short_html()); }

use std::sync::Arc; use crate::clean::cfg::Cfg; <ins> use crate::clean::inline::{load_attrs, merge_attrs}; </ins> use crate::clean::{Crate, Item}; use crate::core::DocContext; use crate::fold::DocFolder; use crate::passes::Pass; <ins> use rustc_hir::def_id::LocalDefId; </ins> pub(crate) const PROPAGATE_DOC_CFG: Pass = Pass { name: "propagate-doc-cfg", run: propagate_doc_cfg, description: "propagates `#[doc(cfg(...))]` to child items", }; <del> pub(crate) fn propagate_doc_cfg(cr: Crate, _: &mut DocContext<'_>) -> Crate { CfgPropagator { parent_cfg: None }.fold_crate(cr) </del> <ins> pub(crate) fn propagate_doc_cfg(cr: Crate, cx: &mut DocContext<'_>) -> Crate { CfgPropagator { parent_cfg: None, parent: None, cx }.fold_crate(cr) </ins> } <del> struct CfgPropagator { </del> <ins> struct CfgPropagator<'a, 'tcx> { </ins> parent_cfg: Option<Arc<Cfg>>, <ins> parent: Option<LocalDefId>, cx: &'a mut DocContext<'tcx>, </ins> } <del> impl DocFolder for CfgPropagator { </del> <ins> impl<'a, 'tcx> DocFolder for CfgPropagator<'a, 'tcx> { </ins> fn fold_item(&mut self, mut item: Item) -> Option<Item> { let old_parent_cfg = self.parent_cfg.clone(); <ins> if item.kind.is_non_assoc() && let Some(def_id) = item.item_id.as_def_id().and_then(|def_id| def_id.as_local()) { let hir = self.cx.tcx.hir(); let hir_id = hir.local_def_id_to_hir_id(def_id); let expected_parent = hir.get_parent_item(hir_id); // If parents are different, it means that `item` is a reexport and we need to compute // the actual `cfg` by iterating through its "real" parents. if self.parent != Some(expected_parent) { let mut attrs = Vec::new(); for (parent_hir_id, _) in hir.parent_iter(hir_id) { let def_id = hir.local_def_id(parent_hir_id).to_def_id(); attrs.extend_from_slice(load_attrs(self.cx, def_id)); } let (_, cfg) = merge_attrs(self.cx, None, item.attrs.other_attrs.as_slice(), Some(&attrs)); item.cfg = cfg; } } </ins> let new_cfg = match (self.parent_cfg.take(), item.cfg.take()) { (None, None) => None, (Some(rc), None) | (None, Some(rc)) => Some(rc),

self.parent_cfg = new_cfg.clone(); item.cfg = new_cfg; <ins> let old_parent = if let Some(def_id) = item.item_id.as_def_id().and_then(|def_id| def_id.as_local()) { self.parent.replace(def_id) } else { self.parent.take() }; </ins> let result = self.fold_item_recur(item); self.parent_cfg = old_parent_cfg; <ins> self.parent = old_parent; </ins> Some(result) }

<ins> #![feature(doc_cfg)] #![feature(no_core)] #![crate_name = "foo"] #![no_core] // @has 'foo/index.html' // @has - '//*[@class="item-left module-item"]/*[@class="stab portability"]' 'foobar' // @has - '//*[@class="item-left module-item"]/*[@class="stab portability"]' 'bar' #[doc(cfg(feature = "foobar"))] mod imp_priv { // @has 'foo/struct.BarPriv.html' // @has - '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]'  //    'Available on crate feature foobar only.' pub struct BarPriv {} impl BarPriv { pub fn test() {} } } #[doc(cfg(feature = "foobar"))] pub use crate::imp_priv::*; pub mod bar { // @has 'foo/bar/struct.Bar.html' // @has - '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]'  //    'Available on crate feature bar only.' #[doc(cfg(feature = "bar"))] pub struct Bar; } #[doc(cfg(feature = "bar"))] pub use bar::Bar; </ins>