Rollup merge of #109422 - notriddle:notriddle/impl-disambiguate-search, r=GuillaumeGomez

rustdoc-search: add impl disambiguator to duplicate assoc items

Preview (to see the difference, click the link and pay attention to the specific function that comes up):

| Before | After |
|--|--|
| [`simd<i64>, simd<i64> -> simd<i64>`](https://doc.rust-lang.org/nightly/std/?search=simd%3Ci64%3E%2C%20simd%3Ci64%3E%20-%3E%20simd%3Ci64%3E) | [`simd<i64>, simd<i64> -> simd<i64>`](https://notriddle.com/rustdoc-demo-html-3/impl-disambiguate-search/std/index.html?search=simd%3Ci64%3E%2C%20simd%3Ci64%3E%20-%3E%20simd%3Ci64%3E) |
| [`cow, vec -> bool`](https://doc.rust-lang.org/nightly/std/?search=cow%2C%20vec%20-%3E%20bool) | [`cow, vec -> bool`](https://notriddle.com/rustdoc-demo-html-3/impl-disambiguate-search/std/index.html?search=cow%2C%20vec%20-%3E%20bool)

Helps with #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.

### The bug

On the "Before" links, this example search calls for `i64`:

![image](9431d89d-41dc-4f68-bbb1-3e2704a973d2)

But if I click any of the results, I get `f64` instead.

![image](6d89c692-1847-421a-84d9-22e359d9cf82)

The PR fixes this problem by adding enough information to the search result `href` to disambiguate methods with different types but the same name.

More detailed description of the problem at:
https://github.com/rust-lang/rust/pull/109422#issuecomment-1491089293

> When a struct/enum/union has multiple impls with different type parameters, it can have multiple methods that have the same name, but which are on different impls. Besides Simd, [Any](https://doc.rust-lang.org/nightly/std/any/trait.Any.html?search=any%3A%3Adowncast) also demonstrates this pattern. It has three methods named `downcast`, on three different impls.
>
> When that happens, it presents a challenge in linking to the method. Normally we link like `#method.foo`. When there are multiple `foo`, we number them like `#method.foo`, `#method.foo-1`, `#method.foo-2`, etc.
>
> It also presents a challenge for our search code. Currently we store all the variants in the index, but don’t have any way to generate unambiguous URLs in the results page, or to distinguish them in the SERP.
>
> To fix this, we need three things:
>
> 1. A fragment format that fully specifies the impl type parameters when needed to disambiguate (`#impl-SimdOrd-for-Simd<i64,+LANES>/method.simd_max`)
> 2. A search index that stores methods with enough information to disambiguate the impl they were on.
> 3. A search results interface that can display multiple methods on the same type with the same name, when appropriate OR a disambiguation landing section on item pages?
>
> For reviewers: it can be hard to see the new fragment format in action since it immediately gets rewritten to the numbered form.
This commit is contained in:
Guillaume Gomez 2023-10-10 18:44:43 +02:00 committed by GitHub
commit 4be9cfabf2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 350 additions and 63 deletions

View file

@ -223,17 +223,16 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
// If the impl is from a masked crate or references something from a // If the impl is from a masked crate or references something from a
// masked crate then remove it completely. // masked crate then remove it completely.
if let clean::ImplItem(ref i) = *item.kind { if let clean::ImplItem(ref i) = *item.kind &&
if self.cache.masked_crates.contains(&item.item_id.krate()) (self.cache.masked_crates.contains(&item.item_id.krate())
|| i.trait_ || i.trait_
.as_ref() .as_ref()
.map_or(false, |t| self.cache.masked_crates.contains(&t.def_id().krate)) .map_or(false, |t| self.cache.masked_crates.contains(&t.def_id().krate))
|| i.for_ || i.for_
.def_id(self.cache) .def_id(self.cache)
.map_or(false, |d| self.cache.masked_crates.contains(&d.krate)) .map_or(false, |d| self.cache.masked_crates.contains(&d.krate)))
{ {
return None; return None;
}
} }
// Propagate a trait method's documentation to all implementors of the // Propagate a trait method's documentation to all implementors of the
@ -334,33 +333,37 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
// A crate has a module at its root, containing all items, // A crate has a module at its root, containing all items,
// which should not be indexed. The crate-item itself is // which should not be indexed. The crate-item itself is
// inserted later on when serializing the search-index. // inserted later on when serializing the search-index.
if item.item_id.as_def_id().map_or(false, |idx| !idx.is_crate_root()) { if item.item_id.as_def_id().map_or(false, |idx| !idx.is_crate_root())
&& let ty = item.type_()
&& (ty != ItemType::StructField
|| u16::from_str_radix(s.as_str(), 10).is_err())
{
let desc = let desc =
short_markdown_summary(&item.doc_value(), &item.link_names(self.cache)); short_markdown_summary(&item.doc_value(), &item.link_names(self.cache));
let ty = item.type_(); // In case this is a field from a tuple struct, we don't add it into
if ty != ItemType::StructField // the search index because its name is something like "0", which is
|| u16::from_str_radix(s.as_str(), 10).is_err() // not useful for rustdoc search.
{ self.cache.search_index.push(IndexItem {
// In case this is a field from a tuple struct, we don't add it into ty,
// the search index because its name is something like "0", which is name: s,
// not useful for rustdoc search. path: join_with_double_colon(path),
self.cache.search_index.push(IndexItem { desc,
ty, parent,
name: s, parent_idx: None,
path: join_with_double_colon(path), impl_id: if let Some(ParentStackItem::Impl { item_id, .. }) = self.cache.parent_stack.last() {
desc, item_id.as_def_id()
parent, } else {
parent_idx: None, None
search_type: get_function_type_for_search( },
&item, search_type: get_function_type_for_search(
self.tcx, &item,
clean_impl_generics(self.cache.parent_stack.last()).as_ref(), self.tcx,
self.cache, clean_impl_generics(self.cache.parent_stack.last()).as_ref(),
), self.cache,
aliases: item.attrs.get_doc_aliases(), ),
deprecation: item.deprecation(self.tcx), aliases: item.attrs.get_doc_aliases(),
}); deprecation: item.deprecation(self.tcx),
} });
} }
} }
(Some(parent), None) if is_inherent_impl_item => { (Some(parent), None) if is_inherent_impl_item => {
@ -371,6 +374,13 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
parent, parent,
item: item.clone(), item: item.clone(),
impl_generics, impl_generics,
impl_id: if let Some(ParentStackItem::Impl { item_id, .. }) =
self.cache.parent_stack.last()
{
item_id.as_def_id()
} else {
None
},
}); });
} }
_ => {} _ => {}
@ -541,6 +551,7 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
pub(crate) struct OrphanImplItem { pub(crate) struct OrphanImplItem {
pub(crate) parent: DefId, pub(crate) parent: DefId,
pub(crate) impl_id: Option<DefId>,
pub(crate) item: clean::Item, pub(crate) item: clean::Item,
pub(crate) impl_generics: Option<(clean::Type, clean::Generics)>, pub(crate) impl_generics: Option<(clean::Type, clean::Generics)>,
} }

View file

@ -54,7 +54,7 @@ use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_hir::def_id::{DefId, DefIdSet}; use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_hir::Mutability; use rustc_hir::Mutability;
use rustc_middle::middle::stability; use rustc_middle::middle::stability;
use rustc_middle::ty::TyCtxt; use rustc_middle::ty::{self, TyCtxt};
use rustc_span::{ use rustc_span::{
symbol::{sym, Symbol}, symbol::{sym, Symbol},
BytePos, FileName, RealFileName, BytePos, FileName, RealFileName,
@ -102,6 +102,7 @@ pub(crate) struct IndexItem {
pub(crate) desc: String, pub(crate) desc: String,
pub(crate) parent: Option<DefId>, pub(crate) parent: Option<DefId>,
pub(crate) parent_idx: Option<isize>, pub(crate) parent_idx: Option<isize>,
pub(crate) impl_id: Option<DefId>,
pub(crate) search_type: Option<IndexItemFunctionType>, pub(crate) search_type: Option<IndexItemFunctionType>,
pub(crate) aliases: Box<[Symbol]>, pub(crate) aliases: Box<[Symbol]>,
pub(crate) deprecation: Option<Deprecation>, pub(crate) deprecation: Option<Deprecation>,
@ -1877,7 +1878,7 @@ pub(crate) fn render_impl_summary(
aliases: &[String], aliases: &[String],
) { ) {
let inner_impl = i.inner_impl(); 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() { let aliases = if aliases.is_empty() {
String::new() String::new()
} else { } else {
@ -1994,21 +1995,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 { fn get_id_for_impl<'tcx>(tcx: TyCtxt<'tcx>, impl_id: ItemId) -> String {
match trait_ { use rustc_middle::ty::print::with_forced_trimmed_paths;
Some(t) => small_url_encode(format!("impl-{:#}-for-{:#}", t.print(cx), for_.print(cx))), let (type_, trait_) = match impl_id {
None => small_url_encode(format!("impl-{:#}", for_.print(cx))), ItemId::Auto { trait_, for_ } => {
} let ty = tcx.type_of(for_).skip_binder();
(ty, Some(ty::TraitRef::new(tcx, trait_, [ty])))
}
ItemId::Blanket { impl_id, .. } | ItemId::DefId(impl_id) => {
match tcx.impl_subject(impl_id).skip_binder() {
ty::ImplSubject::Trait(trait_ref) => {
(trait_ref.args[0].expect_ty(), Some(trait_ref))
}
ty::ImplSubject::Inherent(ty) => (ty, None),
}
}
};
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)> { fn extract_for_impl_name(item: &clean::Item, cx: &Context<'_>) -> Option<(String, String)> {
match *item.kind { match *item.kind {
clean::ItemKind::ImplItem(ref i) => { clean::ItemKind::ImplItem(ref i) if i.trait_.is_some() => {
i.trait_.as_ref().map(|trait_| { // Alternative format produces no URLs,
// Alternative format produces no URLs, // so this parameter does nothing.
// so this parameter does nothing. Some((format!("{:#}", i.for_.print(cx)), get_id_for_impl(cx.tcx(), item.item_id)))
(format!("{:#}", i.for_.print(cx)), get_id_for_impl(&i.for_, Some(trait_), cx))
})
} }
_ => None, _ => None,
} }

View file

@ -12,7 +12,7 @@ use crate::formats::cache::{Cache, OrphanImplItem};
use crate::formats::item_type::ItemType; use crate::formats::item_type::ItemType;
use crate::html::format::join_with_double_colon; use crate::html::format::join_with_double_colon;
use crate::html::markdown::short_markdown_summary; 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 /// Builds the search index from the collected metadata
pub(crate) fn build_index<'tcx>( pub(crate) fn build_index<'tcx>(
@ -26,7 +26,8 @@ pub(crate) fn build_index<'tcx>(
// Attach all orphan items to the type's definition if the type // Attach all orphan items to the type's definition if the type
// has since been learned. // 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) { if let Some((fqp, _)) = cache.paths.get(&parent) {
let desc = short_markdown_summary(&item.doc_value(), &item.link_names(cache)); let desc = short_markdown_summary(&item.doc_value(), &item.link_names(cache));
cache.search_index.push(IndexItem { cache.search_index.push(IndexItem {
@ -36,6 +37,7 @@ pub(crate) fn build_index<'tcx>(
desc, desc,
parent: Some(parent), parent: Some(parent),
parent_idx: None, parent_idx: None,
impl_id,
search_type: get_function_type_for_search(item, tcx, impl_generics.as_ref(), cache), search_type: get_function_type_for_search(item, tcx, impl_generics.as_ref(), cache),
aliases: item.attrs.get_doc_aliases(), aliases: item.attrs.get_doc_aliases(),
deprecation: item.deprecation(tcx), deprecation: item.deprecation(tcx),
@ -222,6 +224,29 @@ pub(crate) fn build_index<'tcx>(
}) })
.collect(); .collect();
// Find associated items that need disambiguators
let mut associated_item_duplicates = FxHashMap::<(isize, 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> { struct CrateData<'a> {
doc: String, doc: String,
items: Vec<&'a IndexItem>, items: Vec<&'a IndexItem>,
@ -230,6 +255,8 @@ pub(crate) fn build_index<'tcx>(
// //
// To be noted: the `usize` elements are indexes to `items`. // To be noted: the `usize` elements are indexes to `items`.
aliases: &'a BTreeMap<String, Vec<usize>>, 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 { struct Paths {
@ -382,6 +409,7 @@ pub(crate) fn build_index<'tcx>(
crate_data.serialize_field("f", &functions)?; crate_data.serialize_field("f", &functions)?;
crate_data.serialize_field("c", &deprecated)?; crate_data.serialize_field("c", &deprecated)?;
crate_data.serialize_field("p", &paths)?; crate_data.serialize_field("p", &paths)?;
crate_data.serialize_field("b", &self.associated_item_disambiguators)?;
if has_aliases { if has_aliases {
crate_data.serialize_field("a", &self.aliases)?; crate_data.serialize_field("a", &self.aliases)?;
} }
@ -398,6 +426,7 @@ pub(crate) fn build_index<'tcx>(
items: crate_items, items: crate_items,
paths: crate_paths, paths: crate_paths,
aliases: &aliases, aliases: &aliases,
associated_item_disambiguators: &associated_item_disambiguators,
}) })
.expect("failed serde conversion") .expect("failed serde conversion")
// All these `replace` calls are because we have to go through JS string for JSON content. // All these `replace` calls are because we have to go through JS string for JSON content.

View file

@ -503,8 +503,7 @@ fn sidebar_render_assoc_items(
.iter() .iter()
.filter_map(|it| { .filter_map(|it| {
let trait_ = it.inner_impl().trait_.as_ref()?; let trait_ = it.inner_impl().trait_.as_ref()?;
let encoded = let encoded = id_map.derive(super::get_id_for_impl(cx.tcx(), it.impl_item.item_id));
id_map.derive(super::get_id_for_impl(&it.inner_impl().for_, Some(trait_), cx));
let prefix = match it.inner_impl().polarity { let prefix = match it.inner_impl().polarity {
ty::ImplPolarity::Positive | ty::ImplPolarity::Reservation => "", ty::ImplPolarity::Positive | ty::ImplPolarity::Reservation => "",

View file

@ -354,6 +354,34 @@ function preLoadCss(cssUrl) {
expandSection(pageId); 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(0, 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)) {
openParentDetails(item);
item.scrollIntoView();
// Let the section expand itself before trying to highlight
setTimeout(() => {
window.location.replace("#" + item.id);
}, 0);
}
}
);
}
}
}
} }
function onHashChange(ev) { function onHashChange(ev) {

View file

@ -1752,6 +1752,7 @@ function initSearch(rawSearchIndex) {
type: item.type, type: item.type,
is_alias: true, is_alias: true,
deprecated: item.deprecated, deprecated: item.deprecated,
implDisambiguator: item.implDisambiguator,
}; };
} }
@ -2218,7 +2219,7 @@ function initSearch(rawSearchIndex) {
href = ROOT_PATH + name + "/index.html"; href = ROOT_PATH + name + "/index.html";
} else if (item.parent !== undefined) { } else if (item.parent !== undefined) {
const myparent = item.parent; const myparent = item.parent;
let anchor = "#" + type + "." + name; let anchor = type + "." + name;
const parentType = itemTypes[myparent.ty]; const parentType = itemTypes[myparent.ty];
let pageType = parentType; let pageType = parentType;
let pageName = myparent.name; let pageName = myparent.name;
@ -2232,16 +2233,19 @@ function initSearch(rawSearchIndex) {
const enumName = item.path.substr(enumNameIdx + 2); const enumName = item.path.substr(enumNameIdx + 2);
path = item.path.substr(0, enumNameIdx); path = item.path.substr(0, enumNameIdx);
displayPath = path + "::" + enumName + "::" + myparent.name + "::"; displayPath = path + "::" + enumName + "::" + myparent.name + "::";
anchor = "#variant." + myparent.name + ".field." + name; anchor = "variant." + myparent.name + ".field." + name;
pageType = "enum"; pageType = "enum";
pageName = enumName; pageName = enumName;
} else { } else {
displayPath = path + "::" + myparent.name + "::"; displayPath = path + "::" + myparent.name + "::";
} }
if (item.implDisambiguator !== null) {
anchor = item.implDisambiguator + "/" + anchor;
}
href = ROOT_PATH + path.replace(/::/g, "/") + href = ROOT_PATH + path.replace(/::/g, "/") +
"/" + pageType + "/" + pageType +
"." + pageName + "." + pageName +
".html" + anchor; ".html#" + anchor;
} else { } else {
displayPath = item.path + "::"; displayPath = item.path + "::";
href = ROOT_PATH + item.path.replace(/::/g, "/") + href = ROOT_PATH + item.path.replace(/::/g, "/") +
@ -2727,6 +2731,10 @@ ${item.displayPath}<span class="${type}">${name}</span>\
* Types are also represented as arrays; the first item is an index into the `p` * 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. * 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` * `a` defines aliases with an Array of pairs: [name, offset], where `offset`
* points into the n/t/d/q/i/f arrays. * points into the n/t/d/q/i/f arrays.
* *
@ -2746,6 +2754,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
* i: Array<Number>, * i: Array<Number>,
* f: Array<RawFunctionSearchType>, * f: Array<RawFunctionSearchType>,
* p: Array<Object>, * p: Array<Object>,
* b: Array<[Number, String]>,
* c: Array<Number> * c: Array<Number>
* }} * }}
*/ */
@ -2766,6 +2775,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
id: id, id: id,
normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""), normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""),
deprecated: null, deprecated: null,
implDisambiguator: null,
}; };
id += 1; id += 1;
searchIndex.push(crateRow); searchIndex.push(crateRow);
@ -2789,6 +2799,8 @@ ${item.displayPath}<span class="${type}">${name}</span>\
const itemFunctionSearchTypes = crateCorpus.f; const itemFunctionSearchTypes = crateCorpus.f;
// an array of (Number) indices for the deprecated items // an array of (Number) indices for the deprecated items
const deprecatedItems = new Set(crateCorpus.c); 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, // an array of [(Number) item type,
// (String) name] // (String) name]
const paths = crateCorpus.p; const paths = crateCorpus.p;
@ -2849,6 +2861,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\
id: id, id: id,
normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""),
deprecated: deprecatedItems.has(i), deprecated: deprecatedItems.has(i),
implDisambiguator: implDisambiguator.has(i) ? implDisambiguator.get(i) : null,
}; };
id += 1; id += 1;
searchIndex.push(row); searchIndex.push(row);

View file

@ -0,0 +1,43 @@
// ignore-tidy-linelength
// Checks that, if a type has two methods with the same name, they both get
// linked correctly.
go-to: "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)
assert: "section:target"
go-to: "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)
assert: "section:target"

View file

@ -529,3 +529,21 @@ pub mod cfgs {
/// Some docs. /// Some docs.
pub mod cfgs {} 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
}
}

View file

@ -0,0 +1,70 @@
// exact-check
// ignore-order
// ignore-tidy-linelength
// This test case verifies that the href points at the correct impl
const FILTER_CRATE = "std";
const EXPECTED = [
{
'query': 'simd<i16>, simd<i16> -> simd<i16>',
'others': [
{
'path': 'std::simd::prelude::Simd',
'name': 'simd_max',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdOrd-for-Simd%3Ci16,+LANES%3E/method.simd_max'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'simd_min',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdOrd-for-Simd%3Ci16,+LANES%3E/method.simd_min'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'simd_clamp',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdOrd-for-Simd%3Ci16,+LANES%3E/method.simd_clamp'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'saturating_add',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdInt-for-Simd%3Ci16,+LANES%3E/method.saturating_add'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'saturating_sub',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdInt-for-Simd%3Ci16,+LANES%3E/method.saturating_sub'
},
],
},
{
'query': 'simd<i8>, simd<i8> -> simd<i8>',
'others': [
{
'path': 'std::simd::prelude::Simd',
'name': 'simd_max',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdOrd-for-Simd%3Ci8,+LANES%3E/method.simd_max'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'simd_min',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdOrd-for-Simd%3Ci8,+LANES%3E/method.simd_min'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'simd_clamp',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdOrd-for-Simd%3Ci8,+LANES%3E/method.simd_clamp'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'saturating_add',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdInt-for-Simd%3Ci8,+LANES%3E/method.saturating_add'
},
{
'path': 'std::simd::prelude::Simd',
'name': 'saturating_sub',
'href': '../std/simd/prelude/struct.Simd.html#impl-SimdInt-for-Simd%3Ci8,+LANES%3E/method.saturating_sub'
},
],
},
];

View file

@ -0,0 +1,28 @@
// exact-check
// ignore-order
// ignore-tidy-linelength
const FILTER_CRATE = "search_method_disambiguate";
const EXPECTED = [
{
'query': 'MyTy -> bool',
'others': [
{
'path': 'search_method_disambiguate::MyTy',
'name': 'my_method',
'href': '../search_method_disambiguate/struct.MyTy.html#impl-X-for-MyTy%3Cbool%3E/method.my_method'
},
],
},
{
'query': 'MyTy -> u8',
'others': [
{
'path': 'search_method_disambiguate::MyTy',
'name': 'my_method',
'href': '../search_method_disambiguate/struct.MyTy.html#impl-X-for-MyTy%3Cu8%3E/method.my_method'
},
],
}
];

View file

@ -0,0 +1,22 @@
pub trait X {
type InnerType;
fn my_method(&self) -> Self::InnerType;
}
pub struct MyTy<T> {
pub t: T,
}
impl X for MyTy<bool> {
type InnerType = bool;
fn my_method(&self) -> bool {
self.t
}
}
impl X for MyTy<u8> {
type InnerType = u8;
fn my_method(&self) -> u8 {
self.t
}
}

View file

@ -1,6 +1,6 @@
#![crate_name = "foo"] #![crate_name = "foo"]
// @has foo/struct.S.html '//*[@id="impl-Into%3CU%3E-for-S"]//h3[@class="code-header"]' 'impl<T, U> Into<U> for T' // @has foo/struct.S.html '//*[@id="impl-Into%3CU%3E-for-T"]//h3[@class="code-header"]' 'impl<T, U> Into<U> for T'
pub struct S2 {} pub struct S2 {}
mod m { mod m {
pub struct S {} pub struct S {}

View file

@ -31,7 +31,7 @@ impl<T> VSet<T, { Order::Unsorted }> {
pub struct Escape<const S: &'static str>; pub struct Escape<const S: &'static str>;
// @has foo/struct.Escape.html '//*[@id="impl-Escape%3Cr%23%22%3Cscript%3Ealert(%22Escape%22);%3C/script%3E%22%23%3E"]/h3[@class="code-header"]' 'impl Escape<r#"<script>alert("Escape");</script>"#>' // @has foo/struct.Escape.html '//*[@id="impl-Escape%3C%22%3Cscript%3Ealert(%5C%22Escape%5C%22);%3C/script%3E%22%3E"]/h3[@class="code-header"]' 'impl Escape<r#"<script>alert("Escape");</script>"#>'
impl Escape<r#"<script>alert("Escape");</script>"#> { impl Escape<r#"<script>alert("Escape");</script>"#> {
pub fn f() {} pub fn f() {}
} }

View file

@ -5,9 +5,9 @@ use std::fmt;
// @!has foo/struct.Bar.html '//*[@id="impl-ToString-for-Bar"]' '' // @!has foo/struct.Bar.html '//*[@id="impl-ToString-for-Bar"]' ''
pub struct Bar; pub struct Bar;
// @has foo/struct.Foo.html '//*[@id="impl-ToString-for-Foo"]//h3[@class="code-header"]' 'impl<T> ToString for T' // @has foo/struct.Foo.html '//*[@id="impl-ToString-for-T"]//h3[@class="code-header"]' 'impl<T> ToString for T'
pub struct Foo; pub struct Foo;
// @has foo/struct.Foo.html '//*[@class="sidebar-elems"]//section//a[@href="#impl-ToString-for-Foo"]' 'ToString' // @has foo/struct.Foo.html '//*[@class="sidebar-elems"]//section//a[@href="#impl-ToString-for-T"]' 'ToString'
impl fmt::Display for Foo { impl fmt::Display for Foo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

View file

@ -22,7 +22,7 @@ impl Bar for GenericStruct<u32> {}
// We check that "Aliased type" is also present as a title in the sidebar. // We check that "Aliased type" is also present as a title in the sidebar.
// @has - '//*[@class="sidebar-elems"]//h3/a[@href="#aliased-type"]' 'Aliased type' // @has - '//*[@class="sidebar-elems"]//h3/a[@href="#aliased-type"]' 'Aliased type'
// We check that we have the implementation of the type alias itself. // We check that we have the implementation of the type alias itself.
// @has - '//*[@id="impl-TypedefStruct"]/h3' 'impl TypedefStruct' // @has - '//*[@id="impl-GenericStruct%3Cu8%3E"]/h3' 'impl TypedefStruct'
// @has - '//*[@id="method.on_alias"]/h4' 'pub fn on_alias()' // @has - '//*[@id="method.on_alias"]/h4' 'pub fn on_alias()'
// @has - '//*[@id="impl-GenericStruct%3CT%3E"]/h3' 'impl<T> GenericStruct<T>' // @has - '//*[@id="impl-GenericStruct%3CT%3E"]/h3' 'impl<T> GenericStruct<T>'
// @has - '//*[@id="method.on_gen"]/h4' 'pub fn on_gen(arg: T)' // @has - '//*[@id="method.on_gen"]/h4' 'pub fn on_gen(arg: T)'

View file

@ -6,7 +6,7 @@
// @has 'foo/struct.AnotherStruct.html' // @has 'foo/struct.AnotherStruct.html'
// @count - '//*[@class="sidebar"]//a[@href="#impl-AnAmazingTrait-for-AnotherStruct%3C()%3E"]' 1 // @count - '//*[@class="sidebar"]//a[@href="#impl-AnAmazingTrait-for-AnotherStruct%3C()%3E"]' 1
// @count - '//*[@class="sidebar"]//a[@href="#impl-AnAmazingTrait-for-AnotherStruct%3CT%3E"]' 1 // @count - '//*[@class="sidebar"]//a[@href="#impl-AnAmazingTrait-for-T"]' 1
pub trait Something {} pub trait Something {}

View file

@ -1,7 +1,7 @@
#![feature(rustc_attrs)] #![feature(rustc_attrs)]
#![crate_name = "foo"] #![crate_name = "foo"]
// @has foo/primitive.i32.html '//*[@id="impl-ToString-for-i32"]//h3[@class="code-header"]' 'impl<T> ToString for T' // @has foo/primitive.i32.html '//*[@id="impl-ToString-for-T"]//h3[@class="code-header"]' 'impl<T> ToString for T'
#[rustc_doc_primitive = "i32"] #[rustc_doc_primitive = "i32"]
/// Some useless docs, wouhou! /// Some useless docs, wouhou!

View file

@ -7,8 +7,8 @@
// @has - '//h2[@id="foreign-impls"]' 'Implementations on Foreign Types' // @has - '//h2[@id="foreign-impls"]' 'Implementations on Foreign Types'
// @has - '//*[@class="sidebar-elems"]//section//a[@href="#impl-Foo-for-u32"]' 'u32' // @has - '//*[@class="sidebar-elems"]//section//a[@href="#impl-Foo-for-u32"]' 'u32'
// @has - '//*[@id="impl-Foo-for-u32"]//h3[@class="code-header"]' 'impl Foo for u32' // @has - '//*[@id="impl-Foo-for-u32"]//h3[@class="code-header"]' 'impl Foo for u32'
// @has - "//*[@class=\"sidebar-elems\"]//section//a[@href=\"#impl-Foo-for-%26'a+str\"]" "&'a str" // @has - "//*[@class=\"sidebar-elems\"]//section//a[@href=\"#impl-Foo-for-%26str\"]" "&'a str"
// @has - "//*[@id=\"impl-Foo-for-%26'a+str\"]//h3[@class=\"code-header\"]" "impl<'a> Foo for &'a str" // @has - "//*[@id=\"impl-Foo-for-%26str\"]//h3[@class=\"code-header\"]" "impl<'a> Foo for &'a str"
pub trait Foo {} pub trait Foo {}
impl Foo for u32 {} impl Foo for u32 {}

View file

@ -5,8 +5,8 @@
// @!has - '//*[@id="impl-Sized-for-Unsized"]//a[@class="src"]' 'source' // @!has - '//*[@id="impl-Sized-for-Unsized"]//a[@class="src"]' 'source'
// @has - '//*[@id="impl-Sync-for-Unsized"]/h3[@class="code-header"]' 'impl Sync for Unsized' // @has - '//*[@id="impl-Sync-for-Unsized"]/h3[@class="code-header"]' 'impl Sync for Unsized'
// @!has - '//*[@id="impl-Sync-for-Unsized"]//a[@class="src"]' 'source' // @!has - '//*[@id="impl-Sync-for-Unsized"]//a[@class="src"]' 'source'
// @has - '//*[@id="impl-Any-for-Unsized"]/h3[@class="code-header"]' 'impl<T> Any for T' // @has - '//*[@id="impl-Any-for-T"]/h3[@class="code-header"]' 'impl<T> Any for T'
// @has - '//*[@id="impl-Any-for-Unsized"]//a[@class="src rightside"]' 'source' // @has - '//*[@id="impl-Any-for-T"]//a[@class="src rightside"]' 'source'
pub struct Unsized { pub struct Unsized {
data: [u8], data: [u8],
} }

View file

@ -7,7 +7,7 @@ where
} }
// @has 'foo/trait.SomeTrait.html' // @has 'foo/trait.SomeTrait.html'
// @has - "//*[@id='impl-SomeTrait%3C(A,+B,+C,+D,+E)%3E-for-(A,+B,+C,+D,+E)']/h3" "impl<A, B, C, D, E> SomeTrait<(A, B, C, D, E)> for (A, B, C, D, E)where A: PartialOrd<A> + PartialEq<A>, B: PartialOrd<B> + PartialEq<B>, C: PartialOrd<C> + PartialEq<C>, D: PartialOrd<D> + PartialEq<D>, E: PartialOrd<E> + PartialEq<E> + ?Sized, " // @has - "//*[@id='impl-SomeTrait-for-(A,+B,+C,+D,+E)']/h3" "impl<A, B, C, D, E> SomeTrait<(A, B, C, D, E)> for (A, B, C, D, E)where A: PartialOrd<A> + PartialEq<A>, B: PartialOrd<B> + PartialEq<B>, C: PartialOrd<C> + PartialEq<C>, D: PartialOrd<D> + PartialEq<D>, E: PartialOrd<E> + PartialEq<E> + ?Sized, "
impl<A, B, C, D, E> SomeTrait<(A, B, C, D, E)> for (A, B, C, D, E) impl<A, B, C, D, E> SomeTrait<(A, B, C, D, E)> for (A, B, C, D, E)
where where
A: PartialOrd<A> + PartialEq<A>, A: PartialOrd<A> + PartialEq<A>,
@ -17,3 +17,14 @@ where
E: PartialOrd<E> + PartialEq<E> + ?Sized, E: PartialOrd<E> + PartialEq<E> + ?Sized,
{ {
} }
// @has - "//*[@id='impl-SomeTrait%3C(A,+B,+C,+D)%3E-for-(A,+B,+C,+D,+E)']/h3" "impl<A, B, C, D, E> SomeTrait<(A, B, C, D)> for (A, B, C, D, E)where A: PartialOrd<A> + PartialEq<A>, B: PartialOrd<B> + PartialEq<B>, C: PartialOrd<C> + PartialEq<C>, D: PartialOrd<D> + PartialEq<D>, E: PartialOrd<E> + PartialEq<E> + ?Sized, "
impl<A, B, C, D, E> SomeTrait<(A, B, C, D)> for (A, B, C, D, E)
where
A: PartialOrd<A> + PartialEq<A>,
B: PartialOrd<B> + PartialEq<B>,
C: PartialOrd<C> + PartialEq<C>,
D: PartialOrd<D> + PartialEq<D>,
E: PartialOrd<E> + PartialEq<E> + ?Sized,
{
}