Skip to content

Commit

Permalink
rustdoc-search: add impl disambiguator to duplicate assoc items
Browse files Browse the repository at this point in the history
Helps with rust-lang#90929

This changes the search results, specifically, when there's more than
one impl with an associated item with the same name. For example,
the search queries `simd<i8> -> simd<i8>` and `simd<i64> -> simd<i64>`
don't link to the same function, but most of the functions have the
same names.

This change should probably be FCP-ed, especially since it adds a new
anchor link format for `main.js` to handle, so that URLs like
`struct.Vec.html#impl-AsMut<[T]>-for-Vec<T,+A>/method.as_mut` redirect
to `struct.Vec.html#method.as_mut-2`. It's a strange design, but there
are a few reasons for it:

* I'd like to avoid making the HTML bigger. Obviously, fixing this bug
  is going to add at least a little more data to the search index, but
  adding more HTML penalises viewers for the benefit of searchers.

* Breaking `struct.Vec.html#method.len` would also be a disappointment.

On the other hand:

* The path-style anchors might be less prone to link rot than the numbered
  anchors. It's definitely less likely to have URLs that appear to "work",
  but silently point at the wrong thing.

* This commit arranges the path-style anchor to redirect to the numbered
  anchor. Nothing stops rustdoc from doing the opposite, making path-style
  anchors the default and redirecting the "legacy" numbered ones.
  • Loading branch information
notriddle committed Sep 11, 2023
1 parent 0dbb7e1 commit d8f8ba3
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 20 deletions.
13 changes: 13 additions & 0 deletions src/librustdoc/formats/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
desc,
parent,
parent_idx: None,
impl_id: if let Some(ParentStackItem::Impl { item_id, .. }) = self.cache.parent_stack.last() {
item_id.as_def_id()
} else {
None
},
search_type: get_function_type_for_search(
&item,
self.tcx,
Expand All @@ -369,6 +374,13 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
parent,
item: item.clone(),
impl_generics,
impl_id: if let Some(ParentStackItem::Impl { item_id, .. }) =
self.cache.parent_stack.last()
{
item_id.as_def_id()
} else {
None
},
});
}
_ => {}
Expand Down Expand Up @@ -539,6 +551,7 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {

pub(crate) struct OrphanImplItem {
pub(crate) parent: DefId,
pub(crate) impl_id: Option<DefId>,
pub(crate) item: clean::Item,
pub(crate) impl_generics: Option<(clean::Type, clean::Generics)>,
}
Expand Down
41 changes: 28 additions & 13 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_hir::Mutability;
use rustc_middle::middle::stability;
use rustc_middle::ty::TyCtxt;
use rustc_middle::ty::{self, TyCtxt};
use rustc_span::{
symbol::{sym, Symbol},
BytePos, FileName, RealFileName,
Expand Down Expand Up @@ -102,6 +102,7 @@ pub(crate) struct IndexItem {
pub(crate) desc: String,
pub(crate) parent: Option<DefId>,
pub(crate) parent_idx: Option<usize>,
pub(crate) impl_id: Option<DefId>,
pub(crate) search_type: Option<IndexItemFunctionType>,
pub(crate) aliases: Box<[Symbol]>,
pub(crate) deprecation: Option<Deprecation>,
Expand Down Expand Up @@ -1861,7 +1862,7 @@ pub(crate) fn render_impl_summary(
aliases: &[String],
) {
let inner_impl = i.inner_impl();
let id = cx.derive_id(get_id_for_impl(&inner_impl.for_, inner_impl.trait_.as_ref(), cx));
let id = cx.derive_id(get_id_for_impl(cx.tcx(), i.impl_item.item_id));
let aliases = if aliases.is_empty() {
String::new()
} else {
Expand Down Expand Up @@ -1978,21 +1979,35 @@ pub(crate) fn small_url_encode(s: String) -> String {
}
}

fn get_id_for_impl(for_: &clean::Type, trait_: Option<&clean::Path>, cx: &Context<'_>) -> String {
match trait_ {
Some(t) => small_url_encode(format!("impl-{:#}-for-{:#}", t.print(cx), for_.print(cx))),
None => small_url_encode(format!("impl-{:#}", for_.print(cx))),
}
fn get_id_for_impl<'tcx>(tcx: TyCtxt<'tcx>, impl_id: ItemId) -> String {
let (type_, trait_) = match impl_id {
ItemId::Auto { trait_, for_ } => {
let ty = tcx.type_of(for_).skip_binder();
(ty, Some(ty::TraitRef::new(tcx, trait_, [ty])))
}
ItemId::Blanket { impl_id, .. } => (
tcx.type_of(impl_id).skip_binder(),
tcx.impl_trait_ref(impl_id).map(|x| x.skip_binder()),
),
ItemId::DefId(impl_id) => (
tcx.type_of(impl_id).skip_binder(),
tcx.impl_trait_ref(impl_id).map(|x| x.skip_binder()),
),
};
use rustc_middle::ty::print::with_forced_trimmed_paths;
with_forced_trimmed_paths!(small_url_encode(if let Some(trait_) = trait_ {
format!("impl-{trait_}-for-{type_}", trait_ = trait_.print_only_trait_path())
} else {
format!("impl-{type_}")
}))
}

fn extract_for_impl_name(item: &clean::Item, cx: &Context<'_>) -> Option<(String, String)> {
match *item.kind {
clean::ItemKind::ImplItem(ref i) => {
i.trait_.as_ref().map(|trait_| {
// Alternative format produces no URLs,
// so this parameter does nothing.
(format!("{:#}", i.for_.print(cx)), get_id_for_impl(&i.for_, Some(trait_), cx))
})
clean::ItemKind::ImplItem(ref i) if i.trait_.is_some() => {
// Alternative format produces no URLs,
// so this parameter does nothing.
Some((format!("{:#}", i.for_.print(cx)), get_id_for_impl(cx.tcx(), item.item_id)))
}
_ => None,
}
Expand Down
33 changes: 31 additions & 2 deletions src/librustdoc/html/render/search_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::formats::cache::{Cache, OrphanImplItem};
use crate::formats::item_type::ItemType;
use crate::html::format::join_with_double_colon;
use crate::html::markdown::short_markdown_summary;
use crate::html::render::{IndexItem, IndexItemFunctionType, RenderType, RenderTypeId};
use crate::html::render::{self, IndexItem, IndexItemFunctionType, RenderType, RenderTypeId};

/// Builds the search index from the collected metadata
pub(crate) fn build_index<'tcx>(
Expand All @@ -26,7 +26,8 @@ pub(crate) fn build_index<'tcx>(

// Attach all orphan items to the type's definition if the type
// has since been learned.
for &OrphanImplItem { parent, ref item, ref impl_generics } in &cache.orphan_impl_items {
for &OrphanImplItem { impl_id, parent, ref item, ref impl_generics } in &cache.orphan_impl_items
{
if let Some((fqp, _)) = cache.paths.get(&parent) {
let desc = short_markdown_summary(&item.doc_value(), &item.link_names(cache));
cache.search_index.push(IndexItem {
Expand All @@ -36,6 +37,7 @@ pub(crate) fn build_index<'tcx>(
desc,
parent: Some(parent),
parent_idx: None,
impl_id,
search_type: get_function_type_for_search(item, tcx, impl_generics.as_ref(), cache),
aliases: item.attrs.get_doc_aliases(),
deprecation: item.deprecation(tcx),
Expand Down Expand Up @@ -210,6 +212,29 @@ pub(crate) fn build_index<'tcx>(
})
.collect();

// Find associated items that need disambiguators
let mut associated_item_duplicates = FxHashMap::<(usize, ItemType, Symbol), usize>::default();

for &item in &crate_items {
if item.impl_id.is_some() && let Some(parent_idx) = item.parent_idx {
let count = associated_item_duplicates
.entry((parent_idx, item.ty, item.name))
.or_insert(0);
*count += 1;
}
}

let associated_item_disambiguators = crate_items
.iter()
.enumerate()
.filter_map(|(index, item)| {
let impl_id = ItemId::DefId(item.impl_id?);
let parent_idx = item.parent_idx?;
let count = *associated_item_duplicates.get(&(parent_idx, item.ty, item.name))?;
if count > 1 { Some((index, render::get_id_for_impl(tcx, impl_id))) } else { None }
})
.collect::<Vec<_>>();

struct CrateData<'a> {
doc: String,
items: Vec<&'a IndexItem>,
Expand All @@ -218,6 +243,8 @@ pub(crate) fn build_index<'tcx>(
//
// To be noted: the `usize` elements are indexes to `items`.
aliases: &'a BTreeMap<String, Vec<usize>>,
// Used when a type has more than one impl with an associated item with the same name.
associated_item_disambiguators: &'a Vec<(usize, String)>,
}

struct Paths {
Expand Down Expand Up @@ -370,6 +397,7 @@ pub(crate) fn build_index<'tcx>(
crate_data.serialize_field("f", &functions)?;
crate_data.serialize_field("c", &deprecated)?;
crate_data.serialize_field("p", &paths)?;
crate_data.serialize_field("b", &self.associated_item_disambiguators)?;
if has_aliases {
crate_data.serialize_field("a", &self.aliases)?;
}
Expand All @@ -386,6 +414,7 @@ pub(crate) fn build_index<'tcx>(
items: crate_items,
paths: crate_paths,
aliases: &aliases,
associated_item_disambiguators: &associated_item_disambiguators,
})
.expect("failed serde conversion")
// All these `replace` calls are because we have to go through JS string for JSON content.
Expand Down
3 changes: 1 addition & 2 deletions src/librustdoc/html/render/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +502,7 @@ fn sidebar_render_assoc_items(
.iter()
.filter_map(|it| {
let trait_ = it.inner_impl().trait_.as_ref()?;
let encoded =
id_map.derive(super::get_id_for_impl(&it.inner_impl().for_, Some(trait_), cx));
let encoded = id_map.derive(super::get_id_for_impl(cx.tcx(), it.impl_item.item_id));

let prefix = match it.inner_impl().polarity {
ty::ImplPolarity::Positive | ty::ImplPolarity::Reservation => "",
Expand Down
24 changes: 24 additions & 0 deletions src/librustdoc/html/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,30 @@ function preLoadCss(cssUrl) {
expandSection(pageId);
}
}
if (savedHash.startsWith("#impl-")) {
// impl-disambiguated links, used by the search engine
// format: impl-X[-for-Y]/method.WHATEVER
// turn this into method.WHATEVER[-NUMBER]
const splitAt = savedHash.indexOf("/");
if (splitAt !== -1) {
const implId = savedHash.slice(1, splitAt);
const assocId = savedHash.slice(splitAt + 1);
const implElem = document.getElementById(implId);
if (implElem && implElem.parentElement.tagName === "SUMMARY" &&
implElem.parentElement.parentElement.tagName === "DETAILS") {
onEachLazy(implElem.parentElement.parentElement.querySelectorAll(
`[id^="${assocId}"]`),
item => {
const numbered = /([^-]+)-([0-9]+)/.exec(item.id);
if (item.id === assocId || (numbered && numbered[1] === assocId)) {
expandSection(item.id);
window.location = "#" + item.id;
}
}
);
}
}
}
}

function onHashChange(ev) {
Expand Down
19 changes: 16 additions & 3 deletions src/librustdoc/html/static/js/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,7 @@ function initSearch(rawSearchIndex) {
type: item.type,
is_alias: true,
deprecated: item.deprecated,
implDisambiguator: item.implDisambiguator,
};
}

Expand Down Expand Up @@ -2061,7 +2062,7 @@ function initSearch(rawSearchIndex) {
href = ROOT_PATH + name + "/index.html";
} else if (item.parent !== undefined) {
const myparent = item.parent;
let anchor = "#" + type + "." + name;
let anchor = type + "." + name;
const parentType = itemTypes[myparent.ty];
let pageType = parentType;
let pageName = myparent.name;
Expand All @@ -2075,16 +2076,19 @@ function initSearch(rawSearchIndex) {
const enumName = item.path.substr(enumNameIdx + 2);
path = item.path.substr(0, enumNameIdx);
displayPath = path + "::" + enumName + "::" + myparent.name + "::";
anchor = "#variant." + myparent.name + ".field." + name;
anchor = "variant." + myparent.name + ".field." + name;
pageType = "enum";
pageName = enumName;
} else {
displayPath = path + "::" + myparent.name + "::";
}
if (item.implDisambiguator !== null) {
anchor = item.implDisambiguator + "/" + anchor;
}
href = ROOT_PATH + path.replace(/::/g, "/") +
"/" + pageType +
"." + pageName +
".html" + anchor;
".html#" + anchor;
} else {
displayPath = item.path + "::";
href = ROOT_PATH + item.path.replace(/::/g, "/") +
Expand Down Expand Up @@ -2562,6 +2566,10 @@ ${item.displayPath}<span class="${type}">${name}</span>\
* Types are also represented as arrays; the first item is an index into the `p`
* array, while the second is a list of types representing any generic parameters.
*
* b[i] contains an item's impl disambiguator. This is only present if an item
* is defined in an impl block and, the impl block's type has more than one associated
* item with the same name.
*
* `a` defines aliases with an Array of pairs: [name, offset], where `offset`
* points into the n/t/d/q/i/f arrays.
*
Expand All @@ -2581,6 +2589,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
* i: Array<Number>,
* f: Array<RawFunctionSearchType>,
* p: Array<Object>,
* b: Array<[Number, String]>,
* c: Array<Number>
* }}
*/
Expand All @@ -2601,6 +2610,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
id: id,
normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""),
deprecated: null,
implDisambiguator: null,
};
id += 1;
searchIndex.push(crateRow);
Expand All @@ -2624,6 +2634,8 @@ ${item.displayPath}<span class="${type}">${name}</span>\
const itemFunctionSearchTypes = crateCorpus.f;
// an array of (Number) indices for the deprecated items
const deprecatedItems = new Set(crateCorpus.c);
// an array of (Number) indices for the deprecated items
const implDisambiguator = new Map(crateCorpus.b);
// an array of [(Number) item type,
// (String) name]
const paths = crateCorpus.p;
Expand Down Expand Up @@ -2684,6 +2696,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
id: id,
normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""),
deprecated: deprecatedItems.has(i),
implDisambiguator: implDisambiguator.has(i) ? implDisambiguator.get(i) : null,
};
id += 1;
searchIndex.push(row);
Expand Down
41 changes: 41 additions & 0 deletions tests/rustdoc-gui/search-result-impl-disambiguation.goml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// ignore-tidy-linelength

// Checks that, if a type has two methods with the same name, they both get
// linked correctly.
goto: "file://" + |DOC_PATH| + "/test_docs/index.html"

// This should link to the inherent impl
write: (".search-input", "ZyxwvutMethodDisambiguation -> bool")
// To be SURE that the search will be run.
press-key: 'Enter'
// Waiting for the search results to appear...
wait-for: "#search-tabs"
// Check the disambiguated link.
assert-count: ("a.result-method", 1)
assert-attribute: ("a.result-method", {
"href": "../test_docs/struct.ZyxwvutMethodDisambiguation.html#impl-ZyxwvutMethodDisambiguation/method.method_impl_disambiguation"
})
click: "a.result-method"
wait-for: "#impl-ZyxwvutMethodDisambiguation"
assert-document-property: ({
"URL": "struct.ZyxwvutMethodDisambiguation.html#method.method_impl_disambiguation"
}, ENDS_WITH)

goto: "file://" + |DOC_PATH| + "/test_docs/index.html"

// This should link to the trait impl
write: (".search-input", "ZyxwvutMethodDisambiguation, usize -> usize")
// To be SURE that the search will be run.
press-key: 'Enter'
// Waiting for the search results to appear...
wait-for: "#search-tabs"
// Check the disambiguated link.
assert-count: ("a.result-method", 1)
assert-attribute: ("a.result-method", {
"href": "../test_docs/struct.ZyxwvutMethodDisambiguation.html#impl-ZyxwvutTrait-for-ZyxwvutMethodDisambiguation/method.method_impl_disambiguation"
})
click: "a.result-method"
wait-for: "#impl-ZyxwvutMethodDisambiguation"
assert-document-property: ({
"URL": "struct.ZyxwvutMethodDisambiguation.html#method.method_impl_disambiguation-1"
}, ENDS_WITH)
18 changes: 18 additions & 0 deletions tests/rustdoc-gui/src/test_docs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,21 @@ pub mod cfgs {
/// Some docs.
pub mod cfgs {}
}

pub struct ZyxwvutMethodDisambiguation;

impl ZyxwvutMethodDisambiguation {
pub fn method_impl_disambiguation(&self) -> bool {
true
}
}

pub trait ZyxwvutTrait {
fn method_impl_disambiguation(&self, x: usize) -> usize;
}

impl ZyxwvutTrait for ZyxwvutMethodDisambiguation {
fn method_impl_disambiguation(&self, x: usize) -> usize {
x
}
}
Loading

0 comments on commit d8f8ba3

Please sign in to comment.