From 7495fdfb59120f621d1255a15eca8a7868576824 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 5 Sep 2026 16:22:40 -0700 Subject: [PATCH 1/7] rustdoc: add missing CCI union logic This fixes a bug that was found where `PathBuf` didn't show up in the standard library search results, because the crate that defined it (libstd) was merged into a crate that already had a path entry in its search index (libproc_macro). --- src/librustdoc/html/render/search_index.rs | 57 ++++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index 0991778f87d79..d709f73eaafe7 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -315,6 +315,29 @@ impl SerializedSearchIndex { let other_entryid_offset = self.names.len(); let mut map_other_pathid_to_self_pathid = Vec::new(); let mut skips = FxHashSet::default(); + + fn remap_entry_data( + other_entry_data: &EntryData, + map_other_pathid_to_self_pathid: &[usize], + ) -> EntryData { + EntryData { + parent: other_entry_data + .parent + .map(|parent| map_other_pathid_to_self_pathid[parent]) + .clone(), + module_path: other_entry_data + .module_path + .map(|path| map_other_pathid_to_self_pathid[path]) + .clone(), + exact_module_path: other_entry_data + .exact_module_path + .map(|exact_path| map_other_pathid_to_self_pathid[exact_path]) + .clone(), + krate: map_other_pathid_to_self_pathid[other_entry_data.krate], + ..other_entry_data.clone() + } + } + for (other_pathid, other_path_data) in other.path_data.iter().enumerate() { if let Some(other_path_data) = other_path_data { let name = Symbol::intern(&other.names[other_pathid]); @@ -442,25 +465,29 @@ impl SerializedSearchIndex { if skips.contains(&other_entryid) { // we push tombstone entries to keep the IDs lined up self.push(String::new(), None, None, String::new(), None, None, None); + if let Some(&self_entryid) = map_other_pathid_to_self_pathid.get(other_entryid) { + // if `self` uses a type that `other` defines, + // copy their definition data into ours + if self.entry_data[self_entryid].is_none() { + self.entry_data[self_entryid] = + other.entry_data[other_entryid].as_ref().map(|other_entry_data| { + remap_entry_data(other_entry_data, &map_other_pathid_to_self_pathid) + }); + } + if self.descs[self_entryid].is_empty() { + self.descs[self_entryid] = other.descs[other_entryid].clone(); + } + assert!( + self.function_data[other_entryid].is_none(), + "this would require a single Entry to be a fn and a path at once", + ); + } } else { self.push( other.names[other_entryid].clone(), other.path_data[other_entryid].clone(), - other.entry_data[other_entryid].as_ref().map(|other_entry_data| EntryData { - parent: other_entry_data - .parent - .map(|parent| map_other_pathid_to_self_pathid[parent]) - .clone(), - module_path: other_entry_data - .module_path - .map(|path| map_other_pathid_to_self_pathid[path]) - .clone(), - exact_module_path: other_entry_data - .exact_module_path - .map(|exact_path| map_other_pathid_to_self_pathid[exact_path]) - .clone(), - krate: map_other_pathid_to_self_pathid[other_entry_data.krate], - ..other_entry_data.clone() + other.entry_data[other_entryid].as_ref().map(|other_entry_data| { + remap_entry_data(other_entry_data, &map_other_pathid_to_self_pathid) }), other.descs[other_entryid].clone(), other.function_data[other_entryid].clone().map(|mut func| { From be3136b71bfb1d1749881fa2e5c8182bd046d294 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 5 Sep 2026 14:58:18 -0700 Subject: [PATCH 2/7] Add regression test for PathBuf not showing up Tests https://github.com/rust-lang/rust/issues/162334 --- tests/rustdoc-js-std/pathbuf.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/rustdoc-js-std/pathbuf.js diff --git a/tests/rustdoc-js-std/pathbuf.js b/tests/rustdoc-js-std/pathbuf.js new file mode 100644 index 0000000000000..d643367f8c487 --- /dev/null +++ b/tests/rustdoc-js-std/pathbuf.js @@ -0,0 +1,9 @@ +const EXPECTED = [ + { + query: 'PathBuf', + others: [ + // ensure hashset::insert comes first + { 'path': 'std::path', 'name': 'PathBuf' }, + ], + }, +]; From 62adee17e57cd012b16946e2afafc31d1264785d Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Sat, 5 Sep 2026 15:43:54 -0700 Subject: [PATCH 3/7] Add regression test for merging search index types --- src/librustdoc/html/render/search_index.rs | 147 ++++++++---------- src/tools/compiletest/src/directives.rs | 4 + .../src/directives/directive_names.rs | 1 + .../compiletest/src/directives/handlers.rs | 7 + src/tools/compiletest/src/runtest.rs | 37 ++++- tests/rustdoc-js/auxiliary/upstream-type.rs | 15 ++ tests/rustdoc-js/downstream-type.js | 52 +++++++ tests/rustdoc-js/downstream-type.rs | 9 ++ 8 files changed, 186 insertions(+), 86 deletions(-) create mode 100644 tests/rustdoc-js/auxiliary/upstream-type.rs create mode 100644 tests/rustdoc-js/downstream-type.js create mode 100644 tests/rustdoc-js/downstream-type.rs diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index d709f73eaafe7..4c93e632ab467 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -462,91 +462,72 @@ impl SerializedSearchIndex { } } for other_entryid in 0..other.names.len() { - if skips.contains(&other_entryid) { - // we push tombstone entries to keep the IDs lined up - self.push(String::new(), None, None, String::new(), None, None, None); - if let Some(&self_entryid) = map_other_pathid_to_self_pathid.get(other_entryid) { - // if `self` uses a type that `other` defines, - // copy their definition data into ours - if self.entry_data[self_entryid].is_none() { - self.entry_data[self_entryid] = - other.entry_data[other_entryid].as_ref().map(|other_entry_data| { - remap_entry_data(other_entry_data, &map_other_pathid_to_self_pathid) - }); - } - if self.descs[self_entryid].is_empty() { - self.descs[self_entryid] = other.descs[other_entryid].clone(); - } - assert!( - self.function_data[other_entryid].is_none(), - "this would require a single Entry to be a fn and a path at once", - ); - } - } else { - self.push( - other.names[other_entryid].clone(), - other.path_data[other_entryid].clone(), - other.entry_data[other_entryid].as_ref().map(|other_entry_data| { - remap_entry_data(other_entry_data, &map_other_pathid_to_self_pathid) - }), - other.descs[other_entryid].clone(), - other.function_data[other_entryid].clone().map(|mut func| { - fn map_fn_sig_item( - map_other_pathid_to_self_pathid: &Vec, - ty: &mut RenderType, - ) { - match ty.id { - None => {} - Some(RenderTypeId::Index(generic)) if generic < 0 => {} - Some(RenderTypeId::Index(id)) => { - let id = usize::try_from(id).unwrap(); - let id = map_other_pathid_to_self_pathid[id]; - assert!(id != !0); - ty.id = Some(RenderTypeId::Index(isize::try_from(id).unwrap())); - } - _ => unreachable!(), + self.push( + other.names[other_entryid].clone(), + if skips.contains(&other_entryid) { + None + } else { + other.path_data[other_entryid].clone() + }, + other.entry_data[other_entryid].as_ref().map(|other_entry_data| { + remap_entry_data(other_entry_data, &map_other_pathid_to_self_pathid) + }), + other.descs[other_entryid].clone(), + other.function_data[other_entryid].clone().map(|mut func| { + fn map_fn_sig_item( + map_other_pathid_to_self_pathid: &Vec, + ty: &mut RenderType, + ) { + match ty.id { + None => {} + Some(RenderTypeId::Index(generic)) if generic < 0 => {} + Some(RenderTypeId::Index(id)) => { + let id = usize::try_from(id).unwrap(); + let id = map_other_pathid_to_self_pathid[id]; + assert!(id != !0); + ty.id = Some(RenderTypeId::Index(isize::try_from(id).unwrap())); } - if let Some(generics) = &mut ty.generics { - for generic in generics { - map_fn_sig_item(map_other_pathid_to_self_pathid, generic); - } + _ => unreachable!(), + } + if let Some(generics) = &mut ty.generics { + for generic in generics { + map_fn_sig_item(map_other_pathid_to_self_pathid, generic); } - if let Some(bindings) = &mut ty.bindings { - for (param, constraints) in bindings { - *param = match *param { - param @ RenderTypeId::Index(generic) if generic < 0 => { - param - } - RenderTypeId::Index(id) => { - let id = usize::try_from(id).unwrap(); - let id = map_other_pathid_to_self_pathid[id]; - assert!(id != !0); - RenderTypeId::Index(isize::try_from(id).unwrap()) - } - _ => unreachable!(), - }; - for constraint in constraints { - map_fn_sig_item( - map_other_pathid_to_self_pathid, - constraint, - ); + } + if let Some(bindings) = &mut ty.bindings { + for (param, constraints) in bindings { + *param = match *param { + param @ RenderTypeId::Index(generic) if generic < 0 => param, + RenderTypeId::Index(id) => { + let id = usize::try_from(id).unwrap(); + let id = map_other_pathid_to_self_pathid[id]; + assert!(id != !0); + RenderTypeId::Index(isize::try_from(id).unwrap()) } + _ => unreachable!(), + }; + for constraint in constraints { + map_fn_sig_item(map_other_pathid_to_self_pathid, constraint); } } } - for input in &mut func.inputs { - map_fn_sig_item(&map_other_pathid_to_self_pathid, input); - } - for output in &mut func.output { - map_fn_sig_item(&map_other_pathid_to_self_pathid, output); - } - for clause in &mut func.where_clause { - for entry in clause { - map_fn_sig_item(&map_other_pathid_to_self_pathid, entry); - } + } + for input in &mut func.inputs { + map_fn_sig_item(&map_other_pathid_to_self_pathid, input); + } + for output in &mut func.output { + map_fn_sig_item(&map_other_pathid_to_self_pathid, output); + } + for clause in &mut func.where_clause { + for entry in clause { + map_fn_sig_item(&map_other_pathid_to_self_pathid, entry); } - func - }), + } + func + }), + if skips.contains(&other_entryid) { + None + } else { other.type_data[other_entryid].as_ref().map(|type_data| TypeData { inverted_function_inputs_index: type_data .inverted_function_inputs_index @@ -583,11 +564,11 @@ impl SerializedSearchIndex { }) .collect(), search_unbox: type_data.search_unbox, - }), - other.alias_pointers[other_entryid] - .map(|alias_pointer| alias_pointer + other_entryid_offset), - ); - } + }) + }, + other.alias_pointers[other_entryid] + .map(|alias_pointer| alias_pointer + other_entryid_offset), + ); } if other.generic_inverted_index.len() > self.generic_inverted_index.len() { self.generic_inverted_index.resize(other.generic_inverted_index.len(), Vec::new()); diff --git a/src/tools/compiletest/src/directives.rs b/src/tools/compiletest/src/directives.rs index 3459273c922a1..24e933c516f81 100644 --- a/src/tools/compiletest/src/directives.rs +++ b/src/tools/compiletest/src/directives.rs @@ -215,6 +215,8 @@ pub(crate) struct TestProps { pub(crate) disable_gdb_pretty_printers: bool, /// Compare the output by lines, rather than as a single string. pub(crate) compare_output_by_lines: bool, + /// Use CCI (`--read-doc-meta` and `--write-doc-meta`) merge mode. + pub(crate) use_rustdoc_cci_doc_meta_merge: bool, } mod directives { @@ -262,6 +264,7 @@ mod directives { pub(crate) const MINICORE_COMPILE_FLAGS: &str = "minicore-compile-flags"; pub(crate) const DISABLE_GDB_PRETTY_PRINTERS: &str = "disable-gdb-pretty-printers"; pub(crate) const COMPARE_OUTPUT_BY_LINES: &str = "compare-output-by-lines"; + pub(crate) const USE_RUSTDOC_CCI_DOC_META_MERGE: &str = "use-rustdoc-cci-doc-meta-merge"; } impl TestProps { @@ -319,6 +322,7 @@ impl TestProps { dont_require_annotations: Default::default(), disable_gdb_pretty_printers: false, compare_output_by_lines: false, + use_rustdoc_cci_doc_meta_merge: false, } } diff --git a/src/tools/compiletest/src/directives/directive_names.rs b/src/tools/compiletest/src/directives/directive_names.rs index d305aaaf9453f..eb7020a00aa29 100644 --- a/src/tools/compiletest/src/directives/directive_names.rs +++ b/src/tools/compiletest/src/directives/directive_names.rs @@ -310,6 +310,7 @@ pub(crate) const KNOWN_DIRECTIVE_NAMES: &[&str] = &[ "unset-rustc-env", // Used by the tidy check `unknown_revision`. "unused-revision-names", + "use-rustdoc-cci-doc-meta-merge", // tidy-alphabetical-end ]; diff --git a/src/tools/compiletest/src/directives/handlers.rs b/src/tools/compiletest/src/directives/handlers.rs index 3848bb4854e75..59656bd15ab15 100644 --- a/src/tools/compiletest/src/directives/handlers.rs +++ b/src/tools/compiletest/src/directives/handlers.rs @@ -364,6 +364,13 @@ fn make_directive_handlers_map() -> HashMap<&'static str, Handler> { &mut props.compare_output_by_lines, ); }), + handler(USE_RUSTDOC_CCI_DOC_META_MERGE, |config, ln, props| { + config.set_name_directive( + ln, + USE_RUSTDOC_CCI_DOC_META_MERGE, + &mut props.use_rustdoc_cci_doc_meta_merge, + ); + }), ]; handlers diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index a08a96f0d7be5..c728e56cf639d 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -1047,14 +1047,21 @@ impl<'test> TestCx<'test> { .args(&self.props.doc_flags); match kind { - DocKind::Html => {} + DocKind::Html => { + if self.props.use_rustdoc_cci_doc_meta_merge { + rustdoc.arg("--write-doc-meta-dir").arg(out_dir.as_ref().join("doc.meta")); + } + } DocKind::Json => { rustdoc.arg("--output-format").arg("json"); } } // Both JSON output and `--disable-minification` are unstable rustdoc options. - if matches!(kind, DocKind::Json) || self.config.disable_minification { + if matches!(kind, DocKind::Json) + || self.config.disable_minification + || self.props.use_rustdoc_cci_doc_meta_merge + { rustdoc.arg("-Zunstable-options"); } if self.config.disable_minification { @@ -1065,7 +1072,31 @@ impl<'test> TestCx<'test> { rustdoc.arg(format!("-Clinker={}", linker)); } - self.compose_and_run_compiler(rustdoc, None) + let docres = self.compose_and_run_compiler(rustdoc, None); + if !docres.status.success() { + return docres; + } + if kind == DocKind::Html && self.props.use_rustdoc_cci_doc_meta_merge { + let mut rustdoc_merge = Command::new(rustdoc_path); + let current_dir = self.output_base_dir(); + rustdoc_merge.current_dir(current_dir); + rustdoc_merge + .arg("-o") + .arg(out_dir.as_ref()) + .args(&self.props.compile_flags) + .args(&self.props.doc_flags) + .arg("--read-doc-meta-dir") + .arg(out_dir.as_ref().join("doc.meta")) + .arg("-Zunstable-options"); + if self.config.disable_minification { + rustdoc_merge.arg("--disable-minification"); + } + let docmerge = self.compose_and_run_compiler(rustdoc_merge, None); + if !docmerge.status.success() { + return docmerge; + } + } + docres } fn exec_compiled_test(&self) -> ProcRes { diff --git a/tests/rustdoc-js/auxiliary/upstream-type.rs b/tests/rustdoc-js/auxiliary/upstream-type.rs new file mode 100644 index 0000000000000..155d9c1626284 --- /dev/null +++ b/tests/rustdoc-js/auxiliary/upstream-type.rs @@ -0,0 +1,15 @@ +//@ use-rustdoc-cci-doc-meta-merge + +/// +pub struct FooBar; + +/// Test case for overlapping struct and function name +#[allow(nonstandard_style)] +pub struct overlapping_name { + _inner: (), +} + +/// Test case for overlapping function and struct name +pub fn overlapping_name() -> FooBar { + FooBar +} diff --git a/tests/rustdoc-js/downstream-type.js b/tests/rustdoc-js/downstream-type.js new file mode 100644 index 0000000000000..ff5d4dd4856ae --- /dev/null +++ b/tests/rustdoc-js/downstream-type.js @@ -0,0 +1,52 @@ +// exact-check +// ignore-order + +// https://github.com/rust-lang/rust/issues/162334 +const EXPECTED = [ + { + 'query': 'FooBar', + 'others': [ + { + 'path': 'upstream_type', + 'name': 'FooBar', + }, + ], + 'in_args': [ + { + 'path': 'downstream_type', + 'name': 'downstream_fn', + 'desc': 'https://github.com/rust-lang/rust/issues/162334', + }, + ], + 'returned': [ + { + 'path': 'upstream_type', + 'name': 'overlapping_name', + 'desc': 'Test case for overlapping function and struct name', + }, + ], + }, + { + 'query': 'overlapping_name', + 'others': [ + { + 'path': 'upstream_type', + 'name': 'overlapping_name', + 'ty': 5, + }, + { + 'path': 'upstream_type', + 'name': 'overlapping_name', + 'ty': 7, + }, + ], + 'returned': [], + 'in_args': [ + { + 'path': 'downstream_type', + 'name': 'with_overlap', + 'desc': '', + }, + ] + }, +]; diff --git a/tests/rustdoc-js/downstream-type.rs b/tests/rustdoc-js/downstream-type.rs new file mode 100644 index 0000000000000..16bba39aa1865 --- /dev/null +++ b/tests/rustdoc-js/downstream-type.rs @@ -0,0 +1,9 @@ +//@ aux-crate:upstream_type=upstream-type.rs +//@ aux-build:upstream-type.rs +//@ build-aux-docs +//@ use-rustdoc-cci-doc-meta-merge + +/// +pub fn downstream_fn(f: upstream_type::FooBar) {} + +pub fn with_overlap(f: upstream_type::overlapping_name) {} From 724f4af2fbfd735bffa017207740bc046ce609b8 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Tue, 8 Sep 2026 11:36:01 -0700 Subject: [PATCH 4/7] Sort files for reproducible doc merge --- src/librustdoc/html/render/write_shared.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index b3c2563aa3f97..ab72edacae296 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -375,15 +375,20 @@ impl CrateInfo { .fold(Ok(Vec::new()), |acc, parts_path| { let mut acc = acc?; let dir = &parts_path.0; - acc.append(&mut try_err!(std::fs::read_dir(dir), dir.as_path()) + let mut files: Vec> = try_err!(std::fs::read_dir(dir), dir.as_path()) + .map(|file| Ok(file?.path())) + .collect(); + files.sort_by_key(|p| p.as_ref().map_or(PathBuf::new(), |p| p.clone())); + acc.append(&mut files + .into_iter() .filter_map(|file| { - let to_crate_info = |file: Result| -> Result, Error> { + let to_crate_info = |file: Result| -> Result, Error> { let file = try_err!(file, dir.as_path()); - if file.path().extension() != Some(OsStr::new("json")) { + if file.extension() != Some(OsStr::new("json")) { return Ok(None); } - let parts = try_err!(fs::read(file.path()), file.path()); - let parts: CrateInfo = try_err!(serde_json::from_slice(&parts), file.path()); + let parts = try_err!(fs::read(&file), &file); + let parts: CrateInfo = try_err!(serde_json::from_slice(&parts), &file); Ok(Some(parts)) }; to_crate_info(file).transpose() From 3a5c7d20d1e3554cb2883f5142345df1aa0d8d50 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 9 Sep 2026 08:03:07 -0700 Subject: [PATCH 5/7] Add comment to downstream-type.js --- tests/rustdoc-js/downstream-type.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/rustdoc-js/downstream-type.js b/tests/rustdoc-js/downstream-type.js index ff5d4dd4856ae..557042bb0ef53 100644 --- a/tests/rustdoc-js/downstream-type.js +++ b/tests/rustdoc-js/downstream-type.js @@ -1,6 +1,12 @@ // exact-check // ignore-order +// The FooBar type is defined in upstream_type, +// but used in downstream_type. This means both crates' +// search indexes contain TypeData for it, +// but only upstream_type defines EntryData. +// This test case ensures we can merge them +// when running in CCI mode. // https://github.com/rust-lang/rust/issues/162334 const EXPECTED = [ { From 937203f68f6a44dbf0f6a0c7ec611409af6c3e0c Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 9 Sep 2026 08:11:23 -0700 Subject: [PATCH 6/7] Add comment to pathbuf.js --- tests/rustdoc-js-std/pathbuf.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/rustdoc-js-std/pathbuf.js b/tests/rustdoc-js-std/pathbuf.js index d643367f8c487..f36196357803c 100644 --- a/tests/rustdoc-js-std/pathbuf.js +++ b/tests/rustdoc-js-std/pathbuf.js @@ -1,3 +1,9 @@ +// The PathBuf type is defined in std, +// but used in proc_macro. This means both crates' +// search indexes contain TypeData for it, +// but only std defines EntryData. +// This test case ensures we can merge them. + const EXPECTED = [ { query: 'PathBuf', From 0a9186a2387f6c171f9e1a22466183360ea23d77 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 9 Sep 2026 08:14:29 -0700 Subject: [PATCH 7/7] Add issue link to pathbuf.js --- tests/rustdoc-js-std/pathbuf.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rustdoc-js-std/pathbuf.js b/tests/rustdoc-js-std/pathbuf.js index f36196357803c..97b11fabe0fb8 100644 --- a/tests/rustdoc-js-std/pathbuf.js +++ b/tests/rustdoc-js-std/pathbuf.js @@ -3,12 +3,14 @@ // search indexes contain TypeData for it, // but only std defines EntryData. // This test case ensures we can merge them. +// +// https://github.com/rust-lang/rust/issues/162334 + const EXPECTED = [ { query: 'PathBuf', others: [ - // ensure hashset::insert comes first { 'path': 'std::path', 'name': 'PathBuf' }, ], },