From f0d002b890a3430e0c61a73cb4708f7fadee94f5 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 2 Feb 2024 17:13:22 +0100 Subject: [PATCH 1/3] Correctly generate path for non-local items in source code pages --- src/librustdoc/formats/item_type.rs | 37 ++++-- src/librustdoc/html/format.rs | 192 ++++++++++++++++++++-------- 2 files changed, 165 insertions(+), 64 deletions(-) diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs index e80da46adb4ca..f10c829bf4eed 100644 --- a/src/librustdoc/formats/item_type.rs +++ b/src/librustdoc/formats/item_type.rs @@ -4,7 +4,7 @@ use std::fmt; use serde::{Serialize, Serializer}; -use rustc_hir::def::DefKind; +use rustc_hir::def::{CtorOf, DefKind}; use rustc_span::hygiene::MacroKind; use crate::clean; @@ -115,7 +115,15 @@ impl<'a> From<&'a clean::Item> for ItemType { impl From for ItemType { fn from(other: DefKind) -> Self { - match other { + Self::from_def_kind(other, None) + } +} + +impl ItemType { + /// Depending on the parent kind, some variants have a different translation (like a `Method` + /// becoming a `TyMethod`). + pub(crate) fn from_def_kind(kind: DefKind, parent_kind: Option) -> Self { + match kind { DefKind::Enum => Self::Enum, DefKind::Fn => Self::Function, DefKind::Mod => Self::Module, @@ -131,30 +139,35 @@ impl From for ItemType { MacroKind::Attr => ItemType::ProcAttribute, MacroKind::Derive => ItemType::ProcDerive, }, - DefKind::ForeignTy - | DefKind::Variant - | DefKind::AssocTy - | DefKind::TyParam + DefKind::ForeignTy => Self::ForeignType, + DefKind::Variant => Self::Variant, + DefKind::Field => Self::StructField, + DefKind::AssocTy => Self::AssocType, + DefKind::AssocFn => { + if let Some(DefKind::Trait) = parent_kind { + Self::TyMethod + } else { + Self::Method + } + } + DefKind::Ctor(CtorOf::Struct, _) => Self::Struct, + DefKind::Ctor(CtorOf::Variant, _) => Self::Variant, + DefKind::AssocConst => Self::AssocConst, + DefKind::TyParam | DefKind::ConstParam - | DefKind::Ctor(..) - | DefKind::AssocFn - | DefKind::AssocConst | DefKind::ExternCrate | DefKind::Use | DefKind::ForeignMod | DefKind::AnonConst | DefKind::InlineConst | DefKind::OpaqueTy - | DefKind::Field | DefKind::LifetimeParam | DefKind::GlobalAsm | DefKind::Impl { .. } | DefKind::Closure => Self::ForeignType, } } -} -impl ItemType { pub(crate) fn as_str(&self) -> &'static str { match *self { ItemType::Module => "mod", diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index 1923fc1511970..6c6d5384882b0 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -32,6 +32,7 @@ use crate::clean::{ self, types::ExternalLocation, utils::find_nearest_parent_module, ExternalCrate, ItemId, PrimitiveType, }; +use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::html::escape::Escape; use crate::html::render::Context; @@ -581,7 +582,7 @@ fn generate_macro_def_id_path( cx: &Context<'_>, root_path: Option<&str>, ) -> Result<(String, ItemType, Vec), HrefError> { - let tcx = cx.shared.tcx; + let tcx = cx.tcx(); let crate_name = tcx.crate_name(def_id.krate); let cache = cx.cache(); @@ -651,92 +652,179 @@ fn generate_macro_def_id_path( Ok((url, ItemType::Macro, fqp)) } +fn generate_item_def_id_path( + mut def_id: DefId, + original_def_id: DefId, + cx: &Context<'_>, + root_path: Option<&str>, + original_def_kind: DefKind, +) -> Result<(String, ItemType, Vec), HrefError> { + use crate::rustc_trait_selection::infer::TyCtxtInferExt; + use crate::rustc_trait_selection::traits::query::normalize::QueryNormalizeExt; + use rustc_middle::traits::ObligationCause; + + let tcx = cx.tcx(); + let crate_name = tcx.crate_name(def_id.krate); + + // 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(); + def_id = infcx + .at(&ObligationCause::dummy(), tcx.param_env(def_id)) + .query_normalize(ty::Binder::dummy(tcx.type_of(def_id).instantiate_identity())) + .map(|resolved| infcx.resolve_vars_if_possible(resolved.value)) + .ok() + .and_then(|normalized| normalized.skip_binder().ty_adt_def()) + .map(|adt| adt.did()) + .unwrap_or(def_id); + } + + let relative: Vec = tcx + .def_path(def_id) + .data + .into_iter() + .filter_map(|elem| { + // extern blocks (and a few others things) have an empty name. + match elem.data.get_opt_name() { + Some(s) if !s.is_empty() => Some(s), + _ => None, + } + }) + .collect(); + let fqp: Vec = once(crate_name).chain(relative).collect(); + + let def_kind = tcx.def_kind(def_id); + let shortty = def_kind.into(); + let module_fqp = to_module_fqp(shortty, &fqp); + let mut is_remote = false; + + let url_parts = url_parts(cx.cache(), def_id, &module_fqp, &cx.current, &mut is_remote)?; + let (url_parts, shortty, fqp) = make_href(root_path, shortty, url_parts, &fqp, is_remote)?; + if def_id == original_def_id { + return Ok((url_parts, shortty, fqp)); + } + let kind = ItemType::from_def_kind(original_def_kind, Some(def_kind)); + Ok((format!("{url_parts}#{kind}.{}", tcx.item_name(original_def_id)), shortty, fqp)) +} + +fn to_module_fqp(shortty: ItemType, fqp: &[Symbol]) -> &[Symbol] { + if shortty == ItemType::Module { fqp } else { &fqp[..fqp.len() - 1] } +} + +fn url_parts( + cache: &Cache, + def_id: DefId, + module_fqp: &[Symbol], + relative_to: &[Symbol], + is_remote: &mut bool, +) -> Result { + match cache.extern_locations[&def_id.krate] { + ExternalLocation::Remote(ref s) => { + *is_remote = true; + let s = s.trim_end_matches('/'); + let mut builder = UrlPartsBuilder::singleton(s); + builder.extend(module_fqp.iter().copied()); + Ok(builder) + } + ExternalLocation::Local => Ok(href_relative_parts(module_fqp, relative_to).collect()), + ExternalLocation::Unknown => Err(HrefError::DocumentationNotBuilt), + } +} + +fn make_href( + root_path: Option<&str>, + shortty: ItemType, + mut url_parts: UrlPartsBuilder, + fqp: &[Symbol], + is_remote: bool, +) -> Result<(String, ItemType, Vec), HrefError> { + if !is_remote && let Some(root_path) = root_path { + let root = root_path.trim_end_matches('/'); + url_parts.push_front(root); + } + debug!(?url_parts); + match shortty { + ItemType::Module => { + url_parts.push("index.html"); + } + _ => { + let prefix = shortty.as_str(); + let last = fqp.last().unwrap(); + url_parts.push_fmt(format_args!("{prefix}.{last}.html")); + } + } + Ok((url_parts.finish(), shortty, fqp.to_vec())) +} + pub(crate) fn href_with_root_path( - did: DefId, + original_did: DefId, cx: &Context<'_>, root_path: Option<&str>, ) -> Result<(String, ItemType, Vec), HrefError> { let tcx = cx.tcx(); - let def_kind = tcx.def_kind(did); + let def_kind = tcx.def_kind(original_did); let did = match def_kind { DefKind::AssocTy | DefKind::AssocFn | DefKind::AssocConst | DefKind::Variant => { // documented on their parent's page - tcx.parent(did) + tcx.parent(original_did) } + // If this a constructor, we get the parent (either a struct or a variant) and then + // generate the link for this item. + DefKind::Ctor(..) => return href_with_root_path(tcx.parent(original_did), cx, root_path), DefKind::ExternCrate => { // Link to the crate itself, not the `extern crate` item. - if let Some(local_did) = did.as_local() { + if let Some(local_did) = original_did.as_local() { tcx.extern_mod_stmt_cnum(local_did).unwrap_or(LOCAL_CRATE).as_def_id() } else { - did + original_did } } - _ => did, + _ => original_did, }; let cache = cx.cache(); let relative_to = &cx.current; - fn to_module_fqp(shortty: ItemType, fqp: &[Symbol]) -> &[Symbol] { - if shortty == ItemType::Module { fqp } else { &fqp[..fqp.len() - 1] } - } - if !did.is_local() - && !cache.effective_visibilities.is_directly_public(tcx, did) - && !cache.document_private - && !cache.primitive_locations.values().any(|&id| id == did) - { - return Err(HrefError::Private); + if !original_did.is_local() { + // 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) { + return Err(HrefError::Private); + } + } else if !cache.effective_visibilities.is_directly_public(tcx, did) + && !cache.document_private + && !cache.primitive_locations.values().any(|&id| id == did) + { + return Err(HrefError::Private); + } } let mut is_remote = false; - let (fqp, shortty, mut url_parts) = match cache.paths.get(&did) { + let (fqp, shortty, url_parts) = match cache.paths.get(&did) { Some(&(ref fqp, shortty)) => (fqp, shortty, { let module_fqp = to_module_fqp(shortty, fqp.as_slice()); debug!(?fqp, ?shortty, ?module_fqp); href_relative_parts(module_fqp, relative_to).collect() }), None => { - if let Some(&(ref fqp, shortty)) = cache.external_paths.get(&did) { + // Associated items are handled differently with "jump to def". The anchor is generated + // directly here whereas for intra-doc links, we have some extra computation being + // performed there. + let def_id_to_get = if root_path.is_some() { original_did } else { did }; + if let Some(&(ref fqp, shortty)) = cache.external_paths.get(&def_id_to_get) { let module_fqp = to_module_fqp(shortty, fqp); - ( - fqp, - shortty, - match cache.extern_locations[&did.krate] { - ExternalLocation::Remote(ref s) => { - is_remote = true; - let s = s.trim_end_matches('/'); - let mut builder = UrlPartsBuilder::singleton(s); - builder.extend(module_fqp.iter().copied()); - builder - } - ExternalLocation::Local => { - href_relative_parts(module_fqp, relative_to).collect() - } - ExternalLocation::Unknown => return Err(HrefError::DocumentationNotBuilt), - }, - ) + (fqp, shortty, url_parts(cache, did, module_fqp, relative_to, &mut is_remote)?) } else if matches!(def_kind, DefKind::Macro(_)) { return generate_macro_def_id_path(did, cx, root_path); + } else if did.is_local() { + return Err(HrefError::Private); } else { - return Err(HrefError::NotInExternalCache); + return generate_item_def_id_path(did, original_did, cx, root_path, def_kind); } } }; - if !is_remote && let Some(root_path) = root_path { - let root = root_path.trim_end_matches('/'); - url_parts.push_front(root); - } - debug!(?url_parts); - match shortty { - ItemType::Module => { - url_parts.push("index.html"); - } - _ => { - let prefix = shortty.as_str(); - let last = fqp.last().unwrap(); - url_parts.push_fmt(format_args!("{prefix}.{last}.html")); - } - } - Ok((url_parts.finish(), shortty, fqp.to_vec())) + make_href(root_path, shortty, url_parts, fqp, is_remote) } pub(crate) fn href( From f3c24833c5af6686e4a1a18a46ed872f27b120ee Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 3 Feb 2024 01:28:08 +0100 Subject: [PATCH 2/3] Add regression test for non local items link generation --- tests/rustdoc/jump-to-non-local-method.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/rustdoc/jump-to-non-local-method.rs diff --git a/tests/rustdoc/jump-to-non-local-method.rs b/tests/rustdoc/jump-to-non-local-method.rs new file mode 100644 index 0000000000000..4f4252a584257 --- /dev/null +++ b/tests/rustdoc/jump-to-non-local-method.rs @@ -0,0 +1,15 @@ +// compile-flags: -Zunstable-options --generate-link-to-definition + +#![crate_name = "foo"] + +use std::sync::atomic::AtomicIsize; + +// @has 'src/foo/jump-to-non-local-method.rs.html' +// @has - '//a[@href="https://doc.rust-lang.org/nightly/core/sync/atomic/struct.AtomicIsize.html#method.new"]' 'AtomicIsize::new' + +pub fn bar() { + let _ = AtomicIsize::new(0); + b(); +} + +fn b() {} From 14e0dab96bb4038fe85930faf7c31227fbf95d61 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 3 Feb 2024 01:35:46 +0100 Subject: [PATCH 3/3] Unify item relative path computation in one function --- src/librustdoc/clean/inline.rs | 19 +++++++++-- src/librustdoc/html/format.rs | 26 ++------------ tests/rustdoc/jump-to-non-local-method.rs | 41 ++++++++++++++++++++--- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index f65c09bf0e810..7ca4392233ed4 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -191,6 +191,20 @@ pub(crate) fn load_attrs<'hir>(cx: &DocContext<'hir>, did: DefId) -> &'hir [ast: cx.tcx.get_attrs_unchecked(did) } +pub(crate) fn item_relative_path(tcx: TyCtxt<'_>, def_id: DefId) -> Vec { + tcx.def_path(def_id) + .data + .into_iter() + .filter_map(|elem| { + // extern blocks (and a few others things) have an empty name. + match elem.data.get_opt_name() { + Some(s) if !s.is_empty() => Some(s), + _ => None, + } + }) + .collect() +} + /// Record an external fully qualified name in the external_paths cache. /// /// These names are used later on by HTML rendering to generate things like @@ -206,8 +220,7 @@ pub(crate) fn record_extern_fqn(cx: &mut DocContext<'_>, did: DefId, kind: ItemT let crate_name = cx.tcx.crate_name(did.krate); - let relative = - cx.tcx.def_path(did).data.into_iter().filter_map(|elem| elem.data.get_opt_name()); + let relative = item_relative_path(cx.tcx, did); let fqn = if let ItemType::Macro = kind { // Check to see if it is a macro 2.0 or built-in macro if matches!( @@ -218,7 +231,7 @@ pub(crate) fn record_extern_fqn(cx: &mut DocContext<'_>, did: DefId, kind: ItemT ) { once(crate_name).chain(relative).collect() } else { - vec![crate_name, relative.last().expect("relative was empty")] + vec![crate_name, *relative.last().expect("relative was empty")] } } else { once(crate_name).chain(relative).collect() diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index 6c6d5384882b0..4ba1665bdc9f1 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -586,18 +586,7 @@ fn generate_macro_def_id_path( let crate_name = tcx.crate_name(def_id.krate); let cache = cx.cache(); - let fqp: Vec = tcx - .def_path(def_id) - .data - .into_iter() - .filter_map(|elem| { - // extern blocks (and a few others things) have an empty name. - match elem.data.get_opt_name() { - Some(s) if !s.is_empty() => Some(s), - _ => None, - } - }) - .collect(); + let fqp = clean::inline::item_relative_path(tcx, def_id); let mut relative = fqp.iter().copied(); let cstore = CStore::from_tcx(tcx); // We need this to prevent a `panic` when this function is used from intra doc links... @@ -680,18 +669,7 @@ fn generate_item_def_id_path( .unwrap_or(def_id); } - let relative: Vec = tcx - .def_path(def_id) - .data - .into_iter() - .filter_map(|elem| { - // extern blocks (and a few others things) have an empty name. - match elem.data.get_opt_name() { - Some(s) if !s.is_empty() => Some(s), - _ => None, - } - }) - .collect(); + let relative = clean::inline::item_relative_path(tcx, def_id); let fqp: Vec = once(crate_name).chain(relative).collect(); let def_kind = tcx.def_kind(def_id); diff --git a/tests/rustdoc/jump-to-non-local-method.rs b/tests/rustdoc/jump-to-non-local-method.rs index 4f4252a584257..7767b92fbe557 100644 --- a/tests/rustdoc/jump-to-non-local-method.rs +++ b/tests/rustdoc/jump-to-non-local-method.rs @@ -2,14 +2,47 @@ #![crate_name = "foo"] +// @has 'src/foo/jump-to-non-local-method.rs.html' + +// @has - '//a[@href="{{channel}}/core/sync/atomic/struct.AtomicIsize.html"]' 'std::sync::atomic::AtomicIsize' use std::sync::atomic::AtomicIsize; +// @has - '//a[@href="{{channel}}/std/io/trait.Read.html"]' 'std::io::Read' +use std::io::Read; +// @has - '//a[@href="{{channel}}/std/io/index.html"]' 'std::io' +use std::io; +// @has - '//a[@href="{{channel}}/std/process/fn.exit.html"]' 'std::process::exit' +use std::process::exit; +use std::cmp::Ordering; +use std::marker::PhantomData; -// @has 'src/foo/jump-to-non-local-method.rs.html' -// @has - '//a[@href="https://doc.rust-lang.org/nightly/core/sync/atomic/struct.AtomicIsize.html#method.new"]' 'AtomicIsize::new' +pub fn bar2(readable: T) { + // @has - '//a[@href="{{channel}}/std/io/trait.Read.html#tymethod.read"]' 'read' + let _ = readable.read(&mut []); +} pub fn bar() { + // @has - '//a[@href="{{channel}}/core/sync/atomic/struct.AtomicIsize.html#method.new"]' 'AtomicIsize::new' let _ = AtomicIsize::new(0); - b(); + // @has - '//a[@href="#48"]' 'local_private' + local_private(); +} + +pub fn extern_call() { + // @has - '//a[@href="{{channel}}/std/process/fn.exit.html"]' 'exit' + exit(0); +} + +pub fn macro_call() -> Result<(), ()> { + // @has - '//a[@href="{{channel}}/core/macro.try.html"]' 'try!' + try!(Err(())); + Ok(()) +} + +pub fn variant() { + // @has - '//a[@href="{{channel}}/core/cmp/enum.Ordering.html#variant.Less"]' 'Ordering::Less' + let _ = Ordering::Less; + // @has - '//a[@href="{{channel}}/core/marker/struct.PhantomData.html"]' 'PhantomData' + let _: PhantomData:: = PhantomData; } -fn b() {} +fn local_private() {}