From b9614b2dc2f0baa65469b28b88077f9775e88d13 Mon Sep 17 00:00:00 2001 From: arferreira Date: Mon, 23 Feb 2026 08:09:08 -0500 Subject: [PATCH 1/3] Fix relative path handling for --extern-html-root-url --- src/librustdoc/clean/types.rs | 11 +++- src/librustdoc/html/format.rs | 52 +++++++++++-------- src/librustdoc/html/render/context.rs | 5 +- src/librustdoc/json/mod.rs | 3 +- .../extern/extern-html-root-url-relative.rs | 15 ++++++ 5 files changed, 60 insertions(+), 26 deletions(-) create mode 100644 tests/rustdoc-html/extern/extern-html-root-url-relative.rs diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 8656378a1e264..b370b2bf68260 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -203,7 +203,14 @@ impl ExternalCrate { if !url.ends_with('/') { url.push('/'); } - Remote(url) + let is_absolute = url.starts_with('/') + || url.split_once("://").is_some_and(|(scheme, _)| { + scheme.bytes().next().is_some_and(|b| b.is_ascii_alphabetic()) + && scheme + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.')) + }); + Remote { url, is_absolute } } // See if there's documentation generated into the local directory @@ -316,7 +323,7 @@ impl ExternalCrate { #[derive(Debug)] pub(crate) enum ExternalLocation { /// Remote URL root of the external crate - Remote(String), + Remote { url: String, is_absolute: bool }, /// This external crate can be found in the local doc/ folder Local, /// The external crate could not be found. diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index 720eb8e5e61df..3fd04449d4ccd 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -402,9 +402,10 @@ fn generate_macro_def_id_path( } let url = match cache.extern_locations[&def_id.krate] { - ExternalLocation::Remote(ref s) => { - // `ExternalLocation::Remote` always end with a `/`. - format!("{s}{path}", path = fmt::from_fn(|f| path.iter().joined("/", f))) + ExternalLocation::Remote { ref url, is_absolute } => { + let mut prefix = remote_url_prefix(url, is_absolute, cx.current.len()); + prefix.extend(path.iter().copied()); + prefix.finish() } ExternalLocation::Local => { // `root_path` always end with a `/`. @@ -458,10 +459,10 @@ fn generate_item_def_id_path( let shortty = ItemType::from_def_id(def_id, tcx); let module_fqp = to_module_fqp(shortty, &fqp); - let mut is_remote = false; + let mut is_absolute = false; - let url_parts = url_parts(cx.cache(), def_id, module_fqp, &cx.current, &mut is_remote)?; - let mut url_parts = make_href(root_path, shortty, url_parts, &fqp, is_remote); + let url_parts = url_parts(cx.cache(), def_id, module_fqp, &cx.current, &mut is_absolute)?; + let mut url_parts = make_href(root_path, shortty, url_parts, &fqp, is_absolute); if def_id != original_def_id { let kind = ItemType::from_def_id(original_def_id, tcx); url_parts = format!("{url_parts}#{kind}.{}", tcx.item_name(original_def_id)) @@ -493,18 +494,29 @@ fn to_module_fqp(shortty: ItemType, fqp: &[Symbol]) -> &[Symbol] { if shortty == ItemType::Module { fqp } else { &fqp[..fqp.len() - 1] } } +fn remote_url_prefix(url: &str, is_absolute: bool, depth: usize) -> UrlPartsBuilder { + let url = url.trim_end_matches('/'); + if is_absolute { + UrlPartsBuilder::singleton(url) + } else { + let extra = depth.saturating_sub(1); + let mut b: UrlPartsBuilder = iter::repeat_n("..", extra).collect(); + b.push(url); + b + } +} + fn url_parts( cache: &Cache, def_id: DefId, module_fqp: &[Symbol], relative_to: &[Symbol], - is_remote: &mut bool, + is_absolute: &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); + ExternalLocation::Remote { ref url, is_absolute: abs } => { + *is_absolute = abs; + let mut builder = remote_url_prefix(url, abs, relative_to.len()); builder.extend(module_fqp.iter().copied()); Ok(builder) } @@ -518,9 +530,9 @@ fn make_href( shortty: ItemType, mut url_parts: UrlPartsBuilder, fqp: &[Symbol], - is_remote: bool, + is_absolute: bool, ) -> String { - if !is_remote && let Some(root_path) = root_path { + if !is_absolute && let Some(root_path) = root_path { let root = root_path.trim_end_matches('/'); url_parts.push_front(root); } @@ -583,7 +595,7 @@ pub(crate) fn href_with_root_path( } } - let mut is_remote = false; + let mut is_absolute = false; 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()); @@ -597,7 +609,7 @@ pub(crate) fn href_with_root_path( 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, url_parts(cache, did, module_fqp, relative_to, &mut is_remote)?) + (fqp, shortty, url_parts(cache, did, module_fqp, relative_to, &mut is_absolute)?) } else if matches!(def_kind, DefKind::Macro(_)) { return generate_macro_def_id_path(did, cx, root_path); } else if did.is_local() { @@ -608,7 +620,7 @@ pub(crate) fn href_with_root_path( } }; Ok(HrefInfo { - url: make_href(root_path, shortty, url_parts, fqp, is_remote), + url: make_href(root_path, shortty, url_parts, fqp, is_absolute), kind: shortty, rust_path: fqp.clone(), }) @@ -762,12 +774,10 @@ fn primitive_link_fragment( } Some(&def_id) => { let loc = match m.extern_locations[&def_id.krate] { - ExternalLocation::Remote(ref s) => { + ExternalLocation::Remote { ref url, is_absolute } => { let cname_sym = ExternalCrate { crate_num: def_id.krate }.name(cx.tcx()); - let builder: UrlPartsBuilder = - [s.as_str().trim_end_matches('/'), cname_sym.as_str()] - .into_iter() - .collect(); + let mut builder = remote_url_prefix(url, is_absolute, cx.current.len()); + builder.push(cname_sym.as_str()); Some(builder) } ExternalLocation::Local => { diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index b0ea8776425f5..4a7b1d1d6c563 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -386,8 +386,9 @@ impl<'tcx> Context<'tcx> { let e = ExternalCrate { crate_num: cnum }; (e.name(self.tcx()), e.src_root(self.tcx())) } - ExternalLocation::Remote(ref s) => { - root = s.to_string(); + ExternalLocation::Remote { ref url, .. } => { + // FIXME: relative extern URLs are not depth-adjusted for source pages + root = url.to_string(); let e = ExternalCrate { crate_num: cnum }; (e.name(self.tcx()), e.src_root(self.tcx())) } diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index a201f661d9f78..7c8e7b7669cd9 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -280,7 +280,8 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> { types::ExternalCrate { name: e.name(self.tcx).to_string(), html_root_url: match external_location { - ExternalLocation::Remote(s) => Some(s.clone()), + // FIXME: relative extern URLs are not resolved here + ExternalLocation::Remote { url, .. } => Some(url.clone()), _ => None, }, path: self diff --git a/tests/rustdoc-html/extern/extern-html-root-url-relative.rs b/tests/rustdoc-html/extern/extern-html-root-url-relative.rs new file mode 100644 index 0000000000000..df6ebf1aedd5a --- /dev/null +++ b/tests/rustdoc-html/extern/extern-html-root-url-relative.rs @@ -0,0 +1,15 @@ +//@ compile-flags:-Z unstable-options --extern-html-root-url core=../ --extern-html-root-takes-precedence + +// At depth 1 (top-level), the href should be ../core/... +//@ has extern_html_root_url_relative/index.html +//@ has - '//a/@href' '../core/iter/index.html' +#[doc(no_inline)] +pub use std::iter; + +// At depth 2 (inside a module), the href should be ../../core/... +pub mod nested { + //@ has extern_html_root_url_relative/nested/index.html + //@ has - '//a/@href' '../../core/iter/index.html' + #[doc(no_inline)] + pub use std::iter; +} From 0da2c0c691f9e9ae984f6ae89a277413a29fc5c3 Mon Sep 17 00:00:00 2001 From: Antonio Souza Date: Mon, 23 Feb 2026 13:55:49 -0500 Subject: [PATCH 2/3] Update src/librustdoc/clean/types.rs Co-authored-by: Michael Howell --- src/librustdoc/clean/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index b370b2bf68260..9557ad3304d4e 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -204,7 +204,7 @@ impl ExternalCrate { url.push('/'); } let is_absolute = url.starts_with('/') - || url.split_once("://").is_some_and(|(scheme, _)| { + || url.split_once(':').is_some_and(|(scheme, _)| { scheme.bytes().next().is_some_and(|b| b.is_ascii_alphabetic()) && scheme .bytes() From b3a41f2385b46a6a29fe302ca6d1fe4190b18523 Mon Sep 17 00:00:00 2001 From: arferreira Date: Tue, 24 Feb 2026 11:41:26 -0500 Subject: [PATCH 3/3] Add test cases for intra-doc links --- src/librustdoc/html/format.rs | 1 + .../extern/extern-html-root-url-relative.rs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index 3fd04449d4ccd..30dacde94cf03 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -532,6 +532,7 @@ fn make_href( fqp: &[Symbol], is_absolute: bool, ) -> String { + // FIXME: relative extern URLs may break when prefixed with root_path if !is_absolute && let Some(root_path) = root_path { let root = root_path.trim_end_matches('/'); url_parts.push_front(root); diff --git a/tests/rustdoc-html/extern/extern-html-root-url-relative.rs b/tests/rustdoc-html/extern/extern-html-root-url-relative.rs index df6ebf1aedd5a..ba2b50c6bf222 100644 --- a/tests/rustdoc-html/extern/extern-html-root-url-relative.rs +++ b/tests/rustdoc-html/extern/extern-html-root-url-relative.rs @@ -1,4 +1,4 @@ -//@ compile-flags:-Z unstable-options --extern-html-root-url core=../ --extern-html-root-takes-precedence +//@ compile-flags:-Z unstable-options --extern-html-root-url core=../ --extern-html-root-takes-precedence --generate-link-to-definition // At depth 1 (top-level), the href should be ../core/... //@ has extern_html_root_url_relative/index.html @@ -9,7 +9,19 @@ pub use std::iter; // At depth 2 (inside a module), the href should be ../../core/... pub mod nested { //@ has extern_html_root_url_relative/nested/index.html - //@ has - '//a/@href' '../../core/iter/index.html' + //@ has - '//a/@href' '../../core/future/index.html' #[doc(no_inline)] - pub use std::iter; + pub use std::future; } + +// Also depth 2, but for an intra-doc link. +//@ has extern_html_root_url_relative/intra_doc_link/index.html +//@ has - '//a/@href' '../../core/ptr/fn.write.html' +/// [write]() +pub mod intra_doc_link { +} + +// link-to-definition +//@ has src/extern_html_root_url_relative/extern-html-root-url-relative.rs.html +//@ has - '//a/@href' '../../core/iter/index.html' +//@ has - '//a/@href' '../../core/future/index.html'