diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index 7f64d20d8e7cd..a6c754ab67f61 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -15,7 +15,7 @@ use rustc_span::hygiene::MacroKind; use rustc_span::symbol::{sym, Symbol}; use rustc_span::Span; -use crate::clean::{self, GetDefId, ToSource, TypeKind}; +use crate::clean::{self, Attributes, GetDefId, ToSource, TypeKind}; use crate::core::DocContext; use crate::doctree; @@ -35,8 +35,11 @@ type Attrs<'hir> = rustc_middle::ty::Attributes<'hir>; /// /// The returned value is `None` if the definition could not be inlined, /// and `Some` of a vector of items if it was successfully expanded. +/// +/// `parent_module` refers to the parent of the *re-export*, not the original item. pub fn try_inline( cx: &DocContext<'_>, + parent_module: DefId, res: Res, name: Symbol, attrs: Option>, @@ -48,12 +51,13 @@ pub fn try_inline( } let mut ret = Vec::new(); + debug!("attrs={:?}", attrs); let attrs_clone = attrs; let inner = match res { Res::Def(DefKind::Trait, did) => { record_extern_fqn(cx, did, clean::TypeKind::Trait); - ret.extend(build_impls(cx, did, attrs)); + ret.extend(build_impls(cx, Some(parent_module), did, attrs)); clean::TraitItem(build_external_trait(cx, did)) } Res::Def(DefKind::Fn, did) => { @@ -62,27 +66,27 @@ pub fn try_inline( } Res::Def(DefKind::Struct, did) => { record_extern_fqn(cx, did, clean::TypeKind::Struct); - ret.extend(build_impls(cx, did, attrs)); + ret.extend(build_impls(cx, Some(parent_module), did, attrs)); clean::StructItem(build_struct(cx, did)) } Res::Def(DefKind::Union, did) => { record_extern_fqn(cx, did, clean::TypeKind::Union); - ret.extend(build_impls(cx, did, attrs)); + ret.extend(build_impls(cx, Some(parent_module), did, attrs)); clean::UnionItem(build_union(cx, did)) } Res::Def(DefKind::TyAlias, did) => { record_extern_fqn(cx, did, clean::TypeKind::Typedef); - ret.extend(build_impls(cx, did, attrs)); + ret.extend(build_impls(cx, Some(parent_module), did, attrs)); clean::TypedefItem(build_type_alias(cx, did), false) } Res::Def(DefKind::Enum, did) => { record_extern_fqn(cx, did, clean::TypeKind::Enum); - ret.extend(build_impls(cx, did, attrs)); + ret.extend(build_impls(cx, Some(parent_module), did, attrs)); clean::EnumItem(build_enum(cx, did)) } Res::Def(DefKind::ForeignTy, did) => { record_extern_fqn(cx, did, clean::TypeKind::Foreign); - ret.extend(build_impls(cx, did, attrs)); + ret.extend(build_impls(cx, Some(parent_module), did, attrs)); clean::ForeignTypeItem } // Never inline enum variants but leave them shown as re-exports. @@ -117,7 +121,7 @@ pub fn try_inline( }; let target_attrs = load_attrs(cx, did); - let attrs = merge_attrs(cx, target_attrs, attrs_clone); + let attrs = merge_attrs(cx, Some(parent_module), target_attrs, attrs_clone); cx.renderinfo.borrow_mut().inlined.insert(did); ret.push(clean::Item { @@ -291,40 +295,52 @@ pub fn build_ty(cx: &DocContext<'_>, did: DefId) -> Option { } /// Builds all inherent implementations of an ADT (struct/union/enum) or Trait item/path/reexport. -pub fn build_impls(cx: &DocContext<'_>, did: DefId, attrs: Option>) -> Vec { +pub fn build_impls( + cx: &DocContext<'_>, + parent_module: Option, + did: DefId, + attrs: Option>, +) -> Vec { let tcx = cx.tcx; let mut impls = Vec::new(); // for each implementation of an item represented by `did`, build the clean::Item for that impl for &did in tcx.inherent_impls(did).iter() { - build_impl(cx, did, attrs, &mut impls); + build_impl(cx, parent_module, did, attrs, &mut impls); } impls } +/// `parent_module` refers to the parent of the re-export, not the original item fn merge_attrs( cx: &DocContext<'_>, - attrs: Attrs<'_>, - other_attrs: Option>, + parent_module: Option, + old_attrs: Attrs<'_>, + new_attrs: Option>, ) -> clean::Attributes { // NOTE: If we have additional attributes (from a re-export), // always insert them first. This ensure that re-export // doc comments show up before the original doc comments // when we render them. - let merged_attrs = if let Some(inner) = other_attrs { - let mut both = inner.to_vec(); - both.extend_from_slice(attrs); - both + if let Some(inner) = new_attrs { + if let Some(new_id) = parent_module { + let diag = cx.sess().diagnostic(); + Attributes::from_ast(diag, old_attrs, Some((inner, new_id))) + } else { + let mut both = inner.to_vec(); + both.extend_from_slice(old_attrs); + both.clean(cx) + } } else { - attrs.to_vec() - }; - merged_attrs.clean(cx) + old_attrs.clean(cx) + } } /// Builds a specific implementation of a type. The `did` could be a type method or trait method. pub fn build_impl( cx: &DocContext<'_>, + parent_module: impl Into>, did: DefId, attrs: Option>, ret: &mut Vec, @@ -333,7 +349,8 @@ pub fn build_impl( return; } - let attrs = merge_attrs(cx, load_attrs(cx, did), attrs); + let attrs = merge_attrs(cx, parent_module.into(), load_attrs(cx, did), attrs); + debug!("merged_attrs={:?}", attrs); let tcx = cx.tcx; let associated_trait = tcx.impl_trait_ref(did); @@ -499,7 +516,9 @@ fn build_module(cx: &DocContext<'_>, did: DefId, visited: &mut FxHashSet) }, )), }); - } else if let Some(i) = try_inline(cx, item.res, item.ident.name, None, visited) { + } else if let Some(i) = + try_inline(cx, did, item.res, item.ident.name, None, visited) + { items.extend(i) } } diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index ac1e2b467045c..ca9135cd11a16 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -284,7 +284,7 @@ impl Clean for doctree::Module<'_> { impl Clean for [ast::Attribute] { fn clean(&self, cx: &DocContext<'_>) -> Attributes { - Attributes::from_ast(cx.sess().diagnostic(), self) + Attributes::from_ast(cx.sess().diagnostic(), self, None) } } @@ -2205,9 +2205,14 @@ impl Clean> for doctree::ExternCrate<'_> { let res = Res::Def(DefKind::Mod, DefId { krate: self.cnum, index: CRATE_DEF_INDEX }); - if let Some(items) = - inline::try_inline(cx, res, self.name, Some(self.attrs), &mut visited) - { + if let Some(items) = inline::try_inline( + cx, + cx.tcx.parent_module(self.hir_id).to_def_id(), + res, + self.name, + Some(self.attrs), + &mut visited, + ) { return items; } } @@ -2268,9 +2273,14 @@ impl Clean> for doctree::Import<'_> { } if !denied { let mut visited = FxHashSet::default(); - if let Some(items) = - inline::try_inline(cx, path.res, name, Some(self.attrs), &mut visited) - { + if let Some(items) = inline::try_inline( + cx, + cx.tcx.parent_module(self.id).to_def_id(), + path.res, + name, + Some(self.attrs), + &mut visited, + ) { return items; } } diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index bb6f449e3550d..179cf248846a8 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -373,6 +373,11 @@ impl> NestedAttributesExt for I { pub struct DocFragment { pub line: usize, pub span: rustc_span::Span, + /// The module this doc-comment came from. + /// + /// This allows distinguishing between the original documentation and a pub re-export. + /// If it is `None`, the item was not re-exported. + pub parent_module: Option, pub doc: String, pub kind: DocFragmentKind, } @@ -386,6 +391,24 @@ pub enum DocFragmentKind { /// A doc fragment created from a `#[doc(include="filename")]` attribute. Contains both the /// given filename and the file contents. Include { filename: String }, + /// A doc fragment used to distinguish between documentation in different modules. + /// + /// In particular, this prevents `collapse_docs` from turning all documentation comments + /// into a single giant attributes even when the item is re-exported with documentation on the re-export. + Divider, +} + +impl DocFragment { + /// Creates a dummy doc-fragment which divides earlier and later fragments. + fn divider() -> Self { + DocFragment { + line: 0, + span: DUMMY_SP, + parent_module: None, + doc: String::new(), + kind: DocFragmentKind::Divider, + } + } } impl<'a> FromIterator<&'a DocFragment> for String { @@ -521,57 +544,77 @@ impl Attributes { false } - pub fn from_ast(diagnostic: &::rustc_errors::Handler, attrs: &[ast::Attribute]) -> Attributes { - let mut doc_strings = vec![]; + pub fn from_ast( + diagnostic: &::rustc_errors::Handler, + attrs: &[ast::Attribute], + additional_attrs: Option<(&[ast::Attribute], DefId)>, + ) -> Attributes { + let doc_strings = RefCell::new(vec![]); let mut sp = None; let mut cfg = Cfg::True; let mut doc_line = 0; - let other_attrs = attrs - .iter() - .filter_map(|attr| { - if let Some(value) = attr.doc_str() { - let value = beautify_doc_string(value); - let kind = if attr.is_doc_comment() { - DocFragmentKind::SugaredDoc - } else { - DocFragmentKind::RawDoc - }; - - let line = doc_line; - doc_line += value.lines().count(); - doc_strings.push(DocFragment { line, span: attr.span, doc: value, kind }); - - if sp.is_none() { - sp = Some(attr.span); - } - None + let clean_attr = |(attr, parent_module): (&ast::Attribute, _)| { + if let Some(value) = attr.doc_str() { + trace!("got doc_str={:?}", value); + let value = beautify_doc_string(value); + let kind = if attr.is_doc_comment() { + DocFragmentKind::SugaredDoc } else { - if attr.has_name(sym::doc) { - if let Some(mi) = attr.meta() { - if let Some(cfg_mi) = Attributes::extract_cfg(&mi) { - // Extracted #[doc(cfg(...))] - match Cfg::parse(cfg_mi) { - Ok(new_cfg) => cfg &= new_cfg, - Err(e) => diagnostic.span_err(e.span, e.msg), - } - } else if let Some((filename, contents)) = - Attributes::extract_include(&mi) - { - let line = doc_line; - doc_line += contents.lines().count(); - doc_strings.push(DocFragment { - line, - span: attr.span, - doc: contents, - kind: DocFragmentKind::Include { filename }, - }); + DocFragmentKind::RawDoc + }; + + let line = doc_line; + doc_line += value.lines().count(); + doc_strings.borrow_mut().push(DocFragment { + line, + span: attr.span, + doc: value, + kind, + parent_module, + }); + + if sp.is_none() { + sp = Some(attr.span); + } + None + } else { + if attr.has_name(sym::doc) { + if let Some(mi) = attr.meta() { + if let Some(cfg_mi) = Attributes::extract_cfg(&mi) { + // Extracted #[doc(cfg(...))] + match Cfg::parse(cfg_mi) { + Ok(new_cfg) => cfg &= new_cfg, + Err(e) => diagnostic.span_err(e.span, e.msg), } + } else if let Some((filename, contents)) = Attributes::extract_include(&mi) + { + let line = doc_line; + doc_line += contents.lines().count(); + doc_strings.borrow_mut().push(DocFragment { + line, + span: attr.span, + doc: contents, + kind: DocFragmentKind::Include { filename }, + parent_module: parent_module, + }); } } - Some(attr.clone()) } + Some(attr.clone()) + } + }; + + // Additional documentation should be shown before the original documentation + let other_attrs = additional_attrs + .into_iter() + .map(|(attrs, id)| { + doc_strings.borrow_mut().push(DocFragment::divider()); + attrs.iter().map(move |attr| (attr, Some(id))) }) + .flatten() + .chain(attrs.iter().map(|attr| (attr, None))) + .filter_map(clean_attr) .collect(); // treat #[target_feature(enable = "feat")] attributes as if they were @@ -597,7 +640,7 @@ impl Attributes { .map_or(true, |a| a.style == AttrStyle::Inner); Attributes { - doc_strings, + doc_strings: doc_strings.into_inner(), other_attrs, cfg: if cfg == Cfg::True { None } else { Some(Arc::new(cfg)) }, span: sp, diff --git a/src/librustdoc/clean/utils.rs b/src/librustdoc/clean/utils.rs index 58b76d24a5bdb..913342e271513 100644 --- a/src/librustdoc/clean/utils.rs +++ b/src/librustdoc/clean/utils.rs @@ -361,7 +361,7 @@ pub fn build_deref_target_impls(cx: &DocContext<'_>, items: &[Item], ret: &mut V let primitive = match *target { ResolvedPath { did, .. } if did.is_local() => continue, ResolvedPath { did, .. } => { - ret.extend(inline::build_impls(cx, did, None)); + ret.extend(inline::build_impls(cx, None, did, None)); continue; } _ => match target.primitive_type() { @@ -371,7 +371,7 @@ pub fn build_deref_target_impls(cx: &DocContext<'_>, items: &[Item], ret: &mut V }; for &did in primitive.impls(tcx) { if !did.is_local() { - inline::build_impl(cx, did, None, ret); + inline::build_impl(cx, None, did, None, ret); } } } diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 7a6c9eabb5f40..eb33890fb5fce 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -927,7 +927,7 @@ impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> { sp: Span, nested: F, ) { - let mut attrs = Attributes::from_ast(self.sess.diagnostic(), attrs); + let mut attrs = Attributes::from_ast(self.sess.diagnostic(), attrs, None); if let Some(ref cfg) = attrs.cfg { if !cfg.matches(&self.sess.parse_sess, Some(&self.sess.features_untracked())) { return; diff --git a/src/librustdoc/doctree.rs b/src/librustdoc/doctree.rs index cfa51dcf4f1d1..6bb9b58bead65 100644 --- a/src/librustdoc/doctree.rs +++ b/src/librustdoc/doctree.rs @@ -8,6 +8,7 @@ use rustc_span::{self, Span, Symbol}; use rustc_hir as hir; use rustc_hir::def_id::CrateNum; +use rustc_hir::HirId; pub struct Module<'hir> { pub name: Option, @@ -236,6 +237,7 @@ pub struct Macro<'hir> { pub struct ExternCrate<'hir> { pub name: Symbol, + pub hir_id: HirId, pub cnum: CrateNum, pub path: Option, pub vis: &'hir hir::Visibility<'hir>, diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index f234dc5c03b31..b9be3e2f92b46 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -8,7 +8,7 @@ use rustc_hir::def::{ Namespace::{self, *}, PerNS, Res, }; -use rustc_hir::def_id::DefId; +use rustc_hir::def_id::{CrateNum, DefId}; use rustc_middle::ty; use rustc_resolve::ParentScope; use rustc_session::lint::{ @@ -767,9 +767,6 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> { self.mod_ids.push(item.def_id); } - let dox = item.attrs.collapsed_doc_value().unwrap_or_else(String::new); - trace!("got documentation '{}'", dox); - // find item's parent to resolve `Self` in item's docs below let parent_name = self.cx.as_local_hir_id(item.def_id).and_then(|item_hir| { let parent_hir = self.cx.tcx.hir().get_parent_item(item_hir); @@ -807,16 +804,53 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> { } }); - for (ori_link, link_range) in markdown_links(&dox) { - self.resolve_link( - &mut item, - &dox, - ¤t_item, - parent_node, - &parent_name, - ori_link, - link_range, - ); + // We want to resolve in the lexical scope of the documentation. + // In the presence of re-exports, this is not the same as the module of the item. + // Rather than merging all documentation into one, resolve it one attribute at a time + // so we know which module it came from. + let mut attrs = item.attrs.doc_strings.iter().peekable(); + while let Some(attr) = attrs.next() { + // `collapse_docs` does not have the behavior we want: + // we want `///` and `#[doc]` to count as the same attribute, + // but currently it will treat them as separate. + // As a workaround, combine all attributes with the same parent module into the same attribute. + let mut combined_docs = attr.doc.clone(); + loop { + match attrs.peek() { + Some(next) if next.parent_module == attr.parent_module => { + combined_docs.push('\n'); + combined_docs.push_str(&attrs.next().unwrap().doc); + } + _ => break, + } + } + debug!("combined_docs={}", combined_docs); + + let (krate, parent_node) = if let Some(id) = attr.parent_module { + trace!("docs {:?} came from {:?}", attr.doc, id); + (id.krate, Some(id)) + } else { + trace!("no parent found for {:?}", attr.doc); + (item.def_id.krate, parent_node) + }; + // NOTE: if there are links that start in one crate and end in another, this will not resolve them. + // This is a degenerate case and it's not supported by rustdoc. + // FIXME: this will break links that start in `#[doc = ...]` and end as a sugared doc. Should this be supported? + for (ori_link, link_range) in markdown_links(&combined_docs) { + let link = self.resolve_link( + &item, + &combined_docs, + ¤t_item, + parent_node, + &parent_name, + krate, + ori_link, + link_range, + ); + if let Some(link) = link { + item.attrs.links.push(link); + } + } } if item.is_mod() && !item.attrs.inner_docs { @@ -838,24 +872,25 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> { impl LinkCollector<'_, '_> { fn resolve_link( &self, - item: &mut Item, + item: &Item, dox: &str, current_item: &Option, parent_node: Option, parent_name: &Option, + krate: CrateNum, ori_link: String, link_range: Option>, - ) { + ) -> Option { trace!("considering link '{}'", ori_link); // Bail early for real links. if ori_link.contains('/') { - return; + return None; } // [] is mostly likely not supposed to be a link if ori_link.is_empty() { - return; + return None; } let cx = self.cx; @@ -863,11 +898,11 @@ impl LinkCollector<'_, '_> { let parts = link.split('#').collect::>(); let (link, extra_fragment) = if parts.len() > 2 { anchor_failure(cx, &item, &link, dox, link_range, AnchorFailure::MultipleAnchors); - return; + return None; } else if parts.len() == 2 { if parts[0].trim().is_empty() { // This is an anchor to an element of the current page, nothing to do in here! - return; + return None; } (parts[0], Some(parts[1].to_owned())) } else { @@ -888,7 +923,7 @@ impl LinkCollector<'_, '_> { .trim(); if path_str.contains(|ch: char| !(ch.is_alphanumeric() || ch == ':' || ch == '_')) { - return; + return None; } // We stripped `()` and `!` when parsing the disambiguator. @@ -928,7 +963,7 @@ impl LinkCollector<'_, '_> { link_range, smallvec![err_kind], ); - return; + return None; }; // replace `Self` with suitable item's parent name @@ -947,7 +982,7 @@ impl LinkCollector<'_, '_> { // (consider `crate::char`). Instead, change it to `self::`. This works because 'self' is now the crate root. resolved_self = format!("self::{}", &path_str["crate::".len()..]); path_str = &resolved_self; - module_id = DefId { krate: item.def_id.krate, index: CRATE_DEF_INDEX }; + module_id = DefId { krate, index: CRATE_DEF_INDEX }; } match self.resolve_with_disambiguator( @@ -962,7 +997,7 @@ impl LinkCollector<'_, '_> { link_range.clone(), ) { Some(x) => x, - None => return, + None => return None, } }; @@ -986,7 +1021,7 @@ impl LinkCollector<'_, '_> { link_range, AnchorFailure::RustdocAnchorConflict(prim), ); - return; + return None; } res = prim; fragment = Some(path.to_owned()); @@ -994,7 +1029,7 @@ impl LinkCollector<'_, '_> { // `[char]` when a `char` module is in scope let candidates = vec![res, prim]; ambiguity_error(cx, &item, path_str, dox, link_range, candidates); - return; + return None; } } } @@ -1018,16 +1053,11 @@ impl LinkCollector<'_, '_> { if let Res::PrimTy(..) = res { match disambiguator { Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => { - item.attrs.links.push(ItemLink { - link: ori_link, - link_text, - did: None, - fragment, - }); + Some(ItemLink { link: ori_link, link_text, did: None, fragment }) } Some(other) => { report_mismatch(other, Disambiguator::Primitive); - return; + None } } } else { @@ -1050,7 +1080,7 @@ impl LinkCollector<'_, '_> { (actual, Some(Disambiguator::Kind(expected))) if actual == expected => {} (_, Some(specified @ Disambiguator::Kind(_) | specified @ Disambiguator::Primitive)) => { report_mismatch(specified, Disambiguator::Kind(kind)); - return; + return None; } } } @@ -1073,14 +1103,14 @@ impl LinkCollector<'_, '_> { } } let id = register_res(cx, res); - item.attrs.links.push(ItemLink { link: ori_link, link_text, did: Some(id), fragment }); + Some(ItemLink { link: ori_link, link_text, did: Some(id), fragment }) } } fn resolve_with_disambiguator( &self, disambiguator: Option, - item: &mut Item, + item: &Item, dox: &str, path_str: &str, current_item: &Option, diff --git a/src/librustdoc/passes/collect_trait_impls.rs b/src/librustdoc/passes/collect_trait_impls.rs index b2c4c30d8ff08..5eb3f98b12371 100644 --- a/src/librustdoc/passes/collect_trait_impls.rs +++ b/src/librustdoc/passes/collect_trait_impls.rs @@ -30,7 +30,7 @@ pub fn collect_trait_impls(krate: Crate, cx: &DocContext<'_>) -> Crate { for &cnum in cx.tcx.crates().iter() { for &(did, _) in cx.tcx.all_trait_implementations(cnum).iter() { cx.tcx.sess.time("build_extern_trait_impl", || { - inline::build_impl(cx, did, None, &mut new_items); + inline::build_impl(cx, None, did, None, &mut new_items); }); } } @@ -38,7 +38,7 @@ pub fn collect_trait_impls(krate: Crate, cx: &DocContext<'_>) -> Crate { // Also try to inline primitive impls from other crates. for &def_id in PrimitiveType::all_impls(cx.tcx).values().flatten() { if !def_id.is_local() { - inline::build_impl(cx, def_id, None, &mut new_items); + inline::build_impl(cx, None, def_id, None, &mut new_items); // FIXME(eddyb) is this `doc(hidden)` check needed? if !cx.tcx.get_attrs(def_id).lists(sym::doc).has_word(sym::hidden) { @@ -90,7 +90,7 @@ pub fn collect_trait_impls(krate: Crate, cx: &DocContext<'_>) -> Crate { for &impl_node in cx.tcx.hir().trait_impls(trait_did) { let impl_did = cx.tcx.hir().local_def_id(impl_node); cx.tcx.sess.time("build_local_trait_impl", || { - inline::build_impl(cx, impl_did.to_def_id(), None, &mut new_items); + inline::build_impl(cx, None, impl_did.to_def_id(), None, &mut new_items); }); } } diff --git a/src/librustdoc/visit_ast.rs b/src/librustdoc/visit_ast.rs index 33578dc0619d1..cbfd2199d9fd6 100644 --- a/src/librustdoc/visit_ast.rs +++ b/src/librustdoc/visit_ast.rs @@ -399,6 +399,7 @@ impl<'a, 'tcx> RustdocVisitor<'a, 'tcx> { om.extern_crates.push(ExternCrate { cnum: self.cx.tcx.extern_mod_stmt_cnum(def_id).unwrap_or(LOCAL_CRATE), name: ident.name, + hir_id: item.hir_id, path: orig_name.map(|x| x.to_string()), vis: &item.vis, attrs: &item.attrs, diff --git a/src/test/rustdoc/intra-link-reexport-additional-docs.rs b/src/test/rustdoc/intra-link-reexport-additional-docs.rs new file mode 100644 index 0000000000000..adb072a7ed555 --- /dev/null +++ b/src/test/rustdoc/intra-link-reexport-additional-docs.rs @@ -0,0 +1,18 @@ +#![crate_name = "foo"] + +// @has foo/struct.JoinPathsError.html '//a[@href="../foo/fn.with_code.html"]' 'crate::with_code' +/// [crate::with_code] +// @has - '//a[@href="../foo/fn.with_code.html"]' 'different text' +/// [different text][with_code] +// @has - '//a[@href="../foo/fn.me_too.html"]' 'me_too' +#[doc = "[me_too]"] +// @has - '//a[@href="../foo/fn.me_three.html"]' 'reference link' +/// This [reference link] +#[doc = "has an attr in the way"] +/// +/// [reference link]: me_three +pub use std::env::JoinPathsError; + +pub fn with_code() {} +pub fn me_too() {} +pub fn me_three() {}