diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index cadfd37f9f35f..4ee91d411903a 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -19,7 +19,7 @@ use rustc_hir::def::{DefKind, MacroKinds};
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
use rustc_hir::{ConstStability, StabilityLevel, StableSince};
use rustc_metadata::creader::CStore;
-use rustc_middle::ty::{self, TyCtxt, TypingMode};
+use rustc_middle::ty::{self, Ty, TyCtxt, TypingMode};
use rustc_span::symbol::kw;
use rustc_span::{Ident, Symbol};
use tracing::{debug, trace};
@@ -410,17 +410,28 @@ fn generate_macro_def_id_path(
Ok(HrefInfo { url, kind: item_type, rust_path: path })
}
+/// Takes an impl `DefId` and return the self `Ty` of the impl.
+fn impl_self_ty(tcx: TyCtxt<'_>, impl_def_id: DefId) -> Ty<'_> {
+ use rustc_middle::traits::ObligationCause;
+ use rustc_middle::ty;
+ use rustc_trait_selection::infer::TyCtxtInferExt;
+ use rustc_trait_selection::traits::query::normalize::QueryNormalizeExt;
+
+ let infcx = tcx.infer_ctxt().build(TypingMode::non_body_analysis());
+ let ty = tcx.type_of(impl_def_id);
+ infcx
+ .at(&ObligationCause::dummy(), tcx.param_env(impl_def_id))
+ .query_normalize(ty::Binder::dummy(ty.instantiate_identity().skip_norm_wip()))
+ .map(|resolved| infcx.deeply_resolve_ignoring_regions(resolved.value).skip_binder())
+ .unwrap_or(ty.skip_binder())
+}
+
fn generate_item_def_id_path(
mut def_id: DefId,
original_def_id: DefId,
cx: &Context<'_>,
root_path: Option<&str>,
) -> Result {
- use rustc_middle::traits::ObligationCause;
- use rustc_middle::ty;
- use rustc_trait_selection::infer::TyCtxtInferExt;
- use rustc_trait_selection::traits::query::normalize::QueryNormalizeExt;
-
let tcx = cx.tcx();
let crate_name = tcx.crate_name(def_id.krate);
let mut prim = None;
@@ -428,13 +439,7 @@ fn generate_item_def_id_path(
// No need to try to infer the actual parent item if it's not an associated item from the `impl`
// block.
if def_id != original_def_id && matches!(tcx.def_kind(def_id), DefKind::Impl { .. }) {
- let infcx = tcx.infer_ctxt().build(TypingMode::non_body_analysis());
- let ty = tcx.type_of(def_id);
- let ty = infcx
- .at(&ObligationCause::dummy(), tcx.param_env(def_id))
- .query_normalize(ty::Binder::dummy(ty.instantiate_identity().skip_norm_wip()))
- .map(|resolved| infcx.deeply_resolve_ignoring_regions(resolved.value).skip_binder())
- .unwrap_or(ty.skip_binder());
+ let ty = impl_self_ty(tcx, def_id);
// If this is a dyn trait, we want to get the actual trait from which the method comes from.
// Since a `dyn trait` (as of 2026) can only be composed of a trait plus auto traits, we
// look for the trait and ignore auto traits.
@@ -552,6 +557,47 @@ fn make_href(
url_parts.finish()
}
+fn ty_inherits_doc_hidden<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> bool {
+ match ty.kind() {
+ // If this is a dyn trait, we want to get the actual trait from which the method comes from.
+ // Since a `dyn trait` (as of 2026) can only be composed of a trait plus auto traits, we
+ // look for the trait and ignore auto traits.
+ ty::Dynamic(traits, _) => traits
+ .iter()
+ .find_map(|trait_| match trait_.skip_binder() {
+ ty::ExistentialPredicate::Trait(t) => Some(inherits_doc_hidden(tcx, t.def_id)),
+ ty::ExistentialPredicate::Projection(p) => {
+ Some(inherits_doc_hidden(tcx, p.trait_ref(tcx).def_id))
+ }
+ ty::ExistentialPredicate::AutoTrait(_) => None,
+ })
+ .unwrap_or(false),
+ ty::Adt(adt, _) => inherits_doc_hidden(tcx, adt.did()),
+ ty::Foreign(def_id) => inherits_doc_hidden(tcx, *def_id),
+ ty::Ref(_, ty, _) => ty_inherits_doc_hidden(tcx, *ty),
+ // For now we consider that everything doesn't inherit `#[doc(hidden)]`. To be confirmed
+ // later.
+ _ => false,
+ }
+}
+
+fn inherits_doc_hidden(tcx: TyCtxt<'_>, mut def_id: DefId) -> bool {
+ loop {
+ if tcx.is_doc_hidden(def_id) {
+ return true;
+ } else if def_id.is_crate_root() {
+ return false;
+ } else if let DefKind::Impl { of_trait } = tcx.def_kind(def_id) {
+ // `impl` blocks stand a bit on their own: unless they have `#[doc(hidden)]` directly
+ // on them, they don't inherit it from the parent context.
+ // Instead we check that the `Self` item and the trait aren't hidden.
+ return ty_inherits_doc_hidden(tcx, impl_self_ty(tcx, def_id))
+ || (of_trait && inherits_doc_hidden(tcx, tcx.impl_trait_id(def_id)));
+ }
+ def_id = tcx.parent(def_id);
+ }
+}
+
pub(crate) fn href_with_root_path(
original_did: DefId,
cx: &Context<'_>,
@@ -590,7 +636,7 @@ pub(crate) fn href_with_root_path(
// If we are generating an href for the "jump to def" feature, then the only case we want
// to ignore is if the item is `doc(hidden)` because we can't link to it.
if root_path.is_some() {
- if tcx.is_doc_hidden(original_did) {
+ if inherits_doc_hidden(tcx, original_did) {
return Err(HrefError::Private);
}
} else if !cache.effective_visibilities.is_directly_public(tcx, did)
diff --git a/tests/rustdoc-html/jump-to-def/auxiliary/hidden-links.rs b/tests/rustdoc-html/jump-to-def/auxiliary/hidden-links.rs
new file mode 100644
index 0000000000000..342a4765f4f7d
--- /dev/null
+++ b/tests/rustdoc-html/jump-to-def/auxiliary/hidden-links.rs
@@ -0,0 +1,19 @@
+#[doc(hidden)]
+pub mod error {
+ pub struct Foo;
+
+ pub trait HiddenTrait {
+ fn hidden(&self) {}
+ }
+}
+
+pub struct Bar;
+pub trait PubTrait {
+ fn public(&self) {}
+}
+
+impl crate::error::HiddenTrait for crate::error::Foo {}
+impl crate::error::HiddenTrait for Bar {}
+
+impl PubTrait for crate::error::Foo {}
+impl PubTrait for Bar {}
diff --git a/tests/rustdoc-html/jump-to-def/hidden-links.rs b/tests/rustdoc-html/jump-to-def/hidden-links.rs
new file mode 100644
index 0000000000000..848265f38f514
--- /dev/null
+++ b/tests/rustdoc-html/jump-to-def/hidden-links.rs
@@ -0,0 +1,43 @@
+// This checks that if the item is not directly `doc(hidden)` but instead a parent
+// is, we don't generate a link as it would lead to non-existing files.
+
+//@ compile-flags: -Zunstable-options --generate-link-to-definition
+//@ aux-build:hidden-links.rs
+//@ build-aux-docs
+
+#![crate_name = "foo"]
+
+extern crate hidden_links;
+
+//@ has 'src/foo/hidden-links.rs.html'
+
+// Because our XPath handler is very bad, I can't look for `not(@id)` so instead,
+// we count how many `` we have in total (line numbers are ``).
+//@ count - '//pre/code/a' 21
+// Then we count the `` of lines. The diff should be the number of non-line `` (no
+// better way to test that either..).
+//@ count - '//pre/code/a[@id]' 13
+
+//@ has - '//pre/code/a[@href="../../hidden_links/trait.PubTrait.html"]' 'PubTrait'
+use hidden_links::{PubTrait, error::HiddenTrait};
+
+fn foo() {
+ // In here, only `Bar` should be linked as `Foo` has a `doc(hidden)` parent and
+ // is not reexported. So there should be only one link.
+
+ // This one should not generate any link.
+ let x = hidden_links::error::Foo;
+ //@ has - '//pre/code/a[@href="../../hidden_links/struct.Bar.html"]' 'Bar'
+ let y = hidden_links::Bar;
+
+ // `x` and `y` generate a link every time...
+ //@ count - '//pre/code/a[@href="#29"]' 2
+ //@ count - '//pre/code/a[@href="#31"]' 2
+ // FIXME(GuillaumeGomez): This is a bug, it should link the trait method on the item's page,
+ // not on the trait's (and so `x.public()`) should not link.
+ //@ count - '//pre/code/a[@href="../../hidden_links/trait.PubTrait.html#method.public"]' 2
+ x.hidden();
+ y.hidden();
+ x.public();
+ y.public();
+}