Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 60 additions & 14 deletions src/librustdoc/html/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -410,31 +410,36 @@ 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<HrefInfo, HrefError> {
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;

// 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.
Expand Down Expand Up @@ -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<'_>,
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions tests/rustdoc-html/jump-to-def/auxiliary/hidden-links.rs
Original file line number Diff line number Diff line change
@@ -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 {}
43 changes: 43 additions & 0 deletions tests/rustdoc-html/jump-to-def/hidden-links.rs
Original file line number Diff line number Diff line change
@@ -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 `<a>` we have in total (line numbers are `<a>`).
//@ count - '//pre/code/a' 21
// Then we count the `<a>` of lines. The diff should be the number of non-line `<a>` (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();
}
Loading