1
Fork 0

Auto merge of #80653 - jryans:doc-deref-recursive, r=jyn514,GuillaumeGomez

Recursively document methods via `Deref` traits

This changes `rustdoc` to recursively follow `Deref` targets so that methods from all levels are added to the rendered output. This implementation displays the methods from all levels in the expanded state with separate sections for each level.

![image](https://user-images.githubusercontent.com/279572/103482863-46723b00-4ddb-11eb-972b-c463351a425c.png)

Fixes https://github.com/rust-lang/rust/issues/26207
Fixes https://github.com/rust-lang/rust/issues/53038
Fixes https://github.com/rust-lang/rust/issues/71640

r? `@jyn514`
This commit is contained in:
bors 2021-01-08 12:34:20 +00:00
commit 937f629535
7 changed files with 276 additions and 122 deletions

View file

@ -1322,7 +1322,6 @@ fn init_id_map() -> FxHashMap<String, usize> {
map.insert("trait-implementations".to_owned(), 1);
map.insert("synthetic-implementations".to_owned(), 1);
map.insert("blanket-implementations".to_owned(), 1);
map.insert("deref-methods".to_owned(), 1);
map
}

View file

@ -115,6 +115,9 @@ crate struct Context<'tcx> {
crate render_redirect_pages: bool,
/// The map used to ensure all generated 'id=' attributes are unique.
id_map: Rc<RefCell<IdMap>>,
/// Tracks section IDs for `Deref` targets so they match in both the main
/// body and the sidebar.
deref_id_map: Rc<RefCell<FxHashMap<DefId, String>>>,
crate shared: Arc<SharedContext<'tcx>>,
all: Rc<RefCell<AllTypes>>,
/// Storage for the errors produced while generating documentation so they
@ -372,7 +375,6 @@ crate fn initial_ids() -> Vec<String> {
"implementors-list",
"synthetic-implementors-list",
"methods",
"deref-methods",
"implementations",
]
.iter()
@ -506,6 +508,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
dst,
render_redirect_pages: false,
id_map: Rc::new(RefCell::new(id_map)),
deref_id_map: Rc::new(RefCell::new(FxHashMap::default())),
shared: Arc::new(scx),
all: Rc::new(RefCell::new(AllTypes::new())),
errors: Rc::new(receiver),
@ -3517,14 +3520,18 @@ fn render_assoc_items(
RenderMode::Normal
}
AssocItemRender::DerefFor { trait_, type_, deref_mut_ } => {
let id =
cx.derive_id(small_url_encode(&format!("deref-methods-{:#}", type_.print())));
cx.deref_id_map.borrow_mut().insert(type_.def_id().unwrap(), id.clone());
write!(
w,
"<h2 id=\"deref-methods\" class=\"small-section-header\">\
Methods from {}&lt;Target = {}&gt;\
<a href=\"#deref-methods\" class=\"anchor\"></a>\
"<h2 id=\"{id}\" class=\"small-section-header\">\
Methods from {trait_}&lt;Target = {type_}&gt;\
<a href=\"#{id}\" class=\"anchor\"></a>\
</h2>",
trait_.print(),
type_.print()
id = id,
trait_ = trait_.print(),
type_ = type_.print(),
);
RenderMode::ForDeref { mut_: deref_mut_ }
}
@ -3548,9 +3555,6 @@ fn render_assoc_items(
);
}
}
if let AssocItemRender::DerefFor { .. } = what {
return;
}
if !traits.is_empty() {
let deref_impl =
traits.iter().find(|t| t.inner_impl().trait_.def_id() == cache.deref_trait_did);
@ -3560,6 +3564,12 @@ fn render_assoc_items(
render_deref_methods(w, cx, impl_, containing_item, has_deref_mut, cache);
}
// If we were already one level into rendering deref methods, we don't want to render
// anything after recursing into any further deref methods above.
if let AssocItemRender::DerefFor { .. } = what {
return;
}
let (synthetic, concrete): (Vec<&&Impl>, Vec<&&Impl>) =
traits.iter().partition(|t| t.inner_impl().synthetic);
let (blanket_impl, concrete): (Vec<&&Impl>, _) =
@ -3631,6 +3641,13 @@ fn render_deref_methods(
let what =
AssocItemRender::DerefFor { trait_: deref_type, type_: real_target, deref_mut_: deref_mut };
if let Some(did) = target.def_id() {
if let Some(type_did) = impl_.inner_impl().for_.def_id() {
// `impl Deref<Target = S> for S`
if did == type_did {
// Avoid infinite cycles
return;
}
}
render_assoc_items(w, cx, container_item, did, what, cache);
} else {
if let Some(prim) = target.primitive_type() {
@ -4165,14 +4182,14 @@ fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buffer, cache:
);
}
match *it.kind {
clean::StructItem(ref s) => sidebar_struct(buffer, it, s),
clean::TraitItem(ref t) => sidebar_trait(buffer, it, t),
clean::PrimitiveItem(_) => sidebar_primitive(buffer, it),
clean::UnionItem(ref u) => sidebar_union(buffer, it, u),
clean::EnumItem(ref e) => sidebar_enum(buffer, it, e),
clean::TypedefItem(_, _) => sidebar_typedef(buffer, it),
clean::StructItem(ref s) => sidebar_struct(cx, buffer, it, s),
clean::TraitItem(ref t) => sidebar_trait(cx, buffer, it, t),
clean::PrimitiveItem(_) => sidebar_primitive(cx, buffer, it),
clean::UnionItem(ref u) => sidebar_union(cx, buffer, it, u),
clean::EnumItem(ref e) => sidebar_enum(cx, buffer, it, e),
clean::TypedefItem(_, _) => sidebar_typedef(cx, buffer, it),
clean::ModuleItem(ref m) => sidebar_module(buffer, &m.items),
clean::ForeignTypeItem => sidebar_foreign_type(buffer, it),
clean::ForeignTypeItem => sidebar_foreign_type(cx, buffer, it),
_ => (),
}
@ -4273,7 +4290,7 @@ fn small_url_encode(s: &str) -> String {
.replace("\"", "%22")
}
fn sidebar_assoc_items(it: &clean::Item) -> String {
fn sidebar_assoc_items(cx: &Context<'_>, it: &clean::Item) -> String {
let mut out = String::new();
let c = cache();
if let Some(v) = c.impls.get(&it.def_id) {
@ -4303,58 +4320,7 @@ fn sidebar_assoc_items(it: &clean::Item) -> String {
.filter(|i| i.inner_impl().trait_.is_some())
.find(|i| i.inner_impl().trait_.def_id() == c.deref_trait_did)
{
debug!("found Deref: {:?}", impl_);
if let Some((target, real_target)) =
impl_.inner_impl().items.iter().find_map(|item| match *item.kind {
clean::TypedefItem(ref t, true) => Some(match *t {
clean::Typedef { item_type: Some(ref type_), .. } => (type_, &t.type_),
_ => (&t.type_, &t.type_),
}),
_ => None,
})
{
debug!("found target, real_target: {:?} {:?}", target, real_target);
let deref_mut = v
.iter()
.filter(|i| i.inner_impl().trait_.is_some())
.any(|i| i.inner_impl().trait_.def_id() == c.deref_mut_trait_did);
let inner_impl = target
.def_id()
.or_else(|| {
target
.primitive_type()
.and_then(|prim| c.primitive_locations.get(&prim).cloned())
})
.and_then(|did| c.impls.get(&did));
if let Some(impls) = inner_impl {
debug!("found inner_impl: {:?}", impls);
out.push_str("<a class=\"sidebar-title\" href=\"#deref-methods\">");
out.push_str(&format!(
"Methods from {}&lt;Target={}&gt;",
Escape(&format!(
"{:#}",
impl_.inner_impl().trait_.as_ref().unwrap().print()
)),
Escape(&format!("{:#}", real_target.print()))
));
out.push_str("</a>");
let mut ret = impls
.iter()
.filter(|i| i.inner_impl().trait_.is_none())
.flat_map(|i| {
get_methods(i.inner_impl(), true, &mut used_links, deref_mut)
})
.collect::<Vec<_>>();
// We want links' order to be reproducible so we don't use unstable sort.
ret.sort();
if !ret.is_empty() {
out.push_str(&format!(
"<div class=\"sidebar-links\">{}</div>",
ret.join("")
));
}
}
}
out.push_str(&sidebar_deref_methods(cx, impl_, v));
}
let format_impls = |impls: Vec<&Impl>| {
let mut links = FxHashSet::default();
@ -4422,7 +4388,81 @@ fn sidebar_assoc_items(it: &clean::Item) -> String {
out
}
fn sidebar_struct(buf: &mut Buffer, it: &clean::Item, s: &clean::Struct) {
fn sidebar_deref_methods(cx: &Context<'_>, impl_: &Impl, v: &Vec<Impl>) -> String {
let mut out = String::new();
let c = cache();
debug!("found Deref: {:?}", impl_);
if let Some((target, real_target)) =
impl_.inner_impl().items.iter().find_map(|item| match *item.kind {
clean::TypedefItem(ref t, true) => Some(match *t {
clean::Typedef { item_type: Some(ref type_), .. } => (type_, &t.type_),
_ => (&t.type_, &t.type_),
}),
_ => None,
})
{
debug!("found target, real_target: {:?} {:?}", target, real_target);
let deref_mut = v
.iter()
.filter(|i| i.inner_impl().trait_.is_some())
.any(|i| i.inner_impl().trait_.def_id() == c.deref_mut_trait_did);
let inner_impl = target
.def_id()
.or_else(|| {
target.primitive_type().and_then(|prim| c.primitive_locations.get(&prim).cloned())
})
.and_then(|did| c.impls.get(&did));
if let Some(impls) = inner_impl {
debug!("found inner_impl: {:?}", impls);
let mut used_links = FxHashSet::default();
let mut ret = impls
.iter()
.filter(|i| i.inner_impl().trait_.is_none())
.flat_map(|i| get_methods(i.inner_impl(), true, &mut used_links, deref_mut))
.collect::<Vec<_>>();
if !ret.is_empty() {
let deref_id_map = cx.deref_id_map.borrow();
let id = deref_id_map
.get(&real_target.def_id().unwrap())
.expect("Deref section without derived id");
out.push_str(&format!(
"<a class=\"sidebar-title\" href=\"#{}\">Methods from {}&lt;Target={}&gt;</a>",
id,
Escape(&format!("{:#}", impl_.inner_impl().trait_.as_ref().unwrap().print())),
Escape(&format!("{:#}", real_target.print())),
));
// We want links' order to be reproducible so we don't use unstable sort.
ret.sort();
out.push_str(&format!("<div class=\"sidebar-links\">{}</div>", ret.join("")));
}
}
// Recurse into any further impls that might exist for `target`
if let Some(target_did) = target.def_id() {
if let Some(target_impls) = c.impls.get(&target_did) {
if let Some(target_deref_impl) = target_impls
.iter()
.filter(|i| i.inner_impl().trait_.is_some())
.find(|i| i.inner_impl().trait_.def_id() == c.deref_trait_did)
{
if let Some(type_did) = impl_.inner_impl().for_.def_id() {
// `impl Deref<Target = S> for S`
if target_did == type_did {
// Avoid infinite cycles
return out;
}
}
out.push_str(&sidebar_deref_methods(cx, target_deref_impl, target_impls));
}
}
}
}
out
}
fn sidebar_struct(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item, s: &clean::Struct) {
let mut sidebar = String::new();
let fields = get_struct_fields_name(&s.fields);
@ -4436,7 +4476,7 @@ fn sidebar_struct(buf: &mut Buffer, it: &clean::Item, s: &clean::Struct) {
}
}
sidebar.push_str(&sidebar_assoc_items(it));
sidebar.push_str(&sidebar_assoc_items(cx, it));
if !sidebar.is_empty() {
write!(buf, "<div class=\"block items\">{}</div>", sidebar);
@ -4467,7 +4507,7 @@ fn is_negative_impl(i: &clean::Impl) -> bool {
i.polarity == Some(clean::ImplPolarity::Negative)
}
fn sidebar_trait(buf: &mut Buffer, it: &clean::Item, t: &clean::Trait) {
fn sidebar_trait(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item, t: &clean::Trait) {
let mut sidebar = String::new();
let mut types = t
@ -4567,7 +4607,7 @@ fn sidebar_trait(buf: &mut Buffer, it: &clean::Item, t: &clean::Trait) {
}
}
sidebar.push_str(&sidebar_assoc_items(it));
sidebar.push_str(&sidebar_assoc_items(cx, it));
sidebar.push_str("<a class=\"sidebar-title\" href=\"#implementors\">Implementors</a>");
if t.is_auto {
@ -4580,16 +4620,16 @@ fn sidebar_trait(buf: &mut Buffer, it: &clean::Item, t: &clean::Trait) {
write!(buf, "<div class=\"block items\">{}</div>", sidebar)
}
fn sidebar_primitive(buf: &mut Buffer, it: &clean::Item) {
let sidebar = sidebar_assoc_items(it);
fn sidebar_primitive(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item) {
let sidebar = sidebar_assoc_items(cx, it);
if !sidebar.is_empty() {
write!(buf, "<div class=\"block items\">{}</div>", sidebar);
}
}
fn sidebar_typedef(buf: &mut Buffer, it: &clean::Item) {
let sidebar = sidebar_assoc_items(it);
fn sidebar_typedef(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item) {
let sidebar = sidebar_assoc_items(cx, it);
if !sidebar.is_empty() {
write!(buf, "<div class=\"block items\">{}</div>", sidebar);
@ -4611,7 +4651,7 @@ fn get_struct_fields_name(fields: &[clean::Item]) -> String {
fields.join("")
}
fn sidebar_union(buf: &mut Buffer, it: &clean::Item, u: &clean::Union) {
fn sidebar_union(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item, u: &clean::Union) {
let mut sidebar = String::new();
let fields = get_struct_fields_name(&u.fields);
@ -4623,14 +4663,14 @@ fn sidebar_union(buf: &mut Buffer, it: &clean::Item, u: &clean::Union) {
));
}
sidebar.push_str(&sidebar_assoc_items(it));
sidebar.push_str(&sidebar_assoc_items(cx, it));
if !sidebar.is_empty() {
write!(buf, "<div class=\"block items\">{}</div>", sidebar);
}
}
fn sidebar_enum(buf: &mut Buffer, it: &clean::Item, e: &clean::Enum) {
fn sidebar_enum(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item, e: &clean::Enum) {
let mut sidebar = String::new();
let mut variants = e
@ -4650,7 +4690,7 @@ fn sidebar_enum(buf: &mut Buffer, it: &clean::Item, e: &clean::Enum) {
));
}
sidebar.push_str(&sidebar_assoc_items(it));
sidebar.push_str(&sidebar_assoc_items(cx, it));
if !sidebar.is_empty() {
write!(buf, "<div class=\"block items\">{}</div>", sidebar);
@ -4739,8 +4779,8 @@ fn sidebar_module(buf: &mut Buffer, items: &[clean::Item]) {
}
}
fn sidebar_foreign_type(buf: &mut Buffer, it: &clean::Item) {
let sidebar = sidebar_assoc_items(it);
fn sidebar_foreign_type(cx: &Context<'_>, buf: &mut Buffer, it: &clean::Item) {
let sidebar = sidebar_assoc_items(cx, it);
if !sidebar.is_empty() {
write!(buf, "<div class=\"block items\">{}</div>", sidebar);
}

View file

@ -3,7 +3,7 @@ use crate::clean::*;
use crate::core::DocContext;
use crate::fold::DocFolder;
use rustc_data_structures::fx::FxHashSet;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
use rustc_middle::ty::DefIdTree;
use rustc_span::symbol::sym;
@ -54,39 +54,6 @@ crate fn collect_trait_impls(krate: Crate, cx: &DocContext<'_>) -> Crate {
}
}
let mut cleaner = BadImplStripper { prims, items: crate_items };
// scan through included items ahead of time to splice in Deref targets to the "valid" sets
for it in &new_items {
if let ImplItem(Impl { ref for_, ref trait_, ref items, .. }) = *it.kind {
if cleaner.keep_item(for_) && trait_.def_id() == cx.tcx.lang_items().deref_trait() {
let target = items
.iter()
.find_map(|item| match *item.kind {
TypedefItem(ref t, true) => Some(&t.type_),
_ => None,
})
.expect("Deref impl without Target type");
if let Some(prim) = target.primitive_type() {
cleaner.prims.insert(prim);
} else if let Some(did) = target.def_id() {
cleaner.items.insert(did);
}
}
}
}
new_items.retain(|it| {
if let ImplItem(Impl { ref for_, ref trait_, ref blanket_impl, .. }) = *it.kind {
cleaner.keep_item(for_)
|| trait_.as_ref().map_or(false, |t| cleaner.keep_item(t))
|| blanket_impl.is_some()
} else {
true
}
});
// `tcx.crates()` doesn't include the local crate, and `tcx.all_trait_implementations`
// doesn't work with it anyway, so pull them from the HIR map instead
for &trait_did in cx.tcx.all_traits(LOCAL_CRATE).iter() {
@ -123,6 +90,63 @@ crate fn collect_trait_impls(krate: Crate, cx: &DocContext<'_>) -> Crate {
}
}
let mut cleaner = BadImplStripper { prims, items: crate_items };
let mut type_did_to_deref_target: FxHashMap<DefId, &Type> = FxHashMap::default();
// Gather all type to `Deref` target edges.
for it in &new_items {
if let ImplItem(Impl { ref for_, ref trait_, ref items, .. }) = *it.kind {
if trait_.def_id() == cx.tcx.lang_items().deref_trait() {
let target = items.iter().find_map(|item| match *item.kind {
TypedefItem(ref t, true) => Some(&t.type_),
_ => None,
});
if let (Some(for_did), Some(target)) = (for_.def_id(), target) {
type_did_to_deref_target.insert(for_did, target);
}
}
}
}
// Follow all `Deref` targets of included items and recursively add them as valid
fn add_deref_target(
map: &FxHashMap<DefId, &Type>,
cleaner: &mut BadImplStripper,
type_did: &DefId,
) {
if let Some(target) = map.get(type_did) {
debug!("add_deref_target: type {:?}, target {:?}", type_did, target);
if let Some(target_prim) = target.primitive_type() {
cleaner.prims.insert(target_prim);
} else if let Some(target_did) = target.def_id() {
// `impl Deref<Target = S> for S`
if target_did == *type_did {
// Avoid infinite cycles
return;
}
cleaner.items.insert(target_did);
add_deref_target(map, cleaner, &target_did);
}
}
}
for type_did in type_did_to_deref_target.keys() {
// Since only the `DefId` portion of the `Type` instances is known to be same for both the
// `Deref` target type and the impl for type positions, this map of types is keyed by
// `DefId` and for convenience uses a special cleaner that accepts `DefId`s directly.
if cleaner.keep_impl_with_def_id(type_did) {
add_deref_target(&type_did_to_deref_target, &mut cleaner, type_did);
}
}
new_items.retain(|it| {
if let ImplItem(Impl { ref for_, ref trait_, ref blanket_impl, .. }) = *it.kind {
cleaner.keep_impl(for_)
|| trait_.as_ref().map_or(false, |t| cleaner.keep_impl(t))
|| blanket_impl.is_some()
} else {
true
}
});
if let Some(ref mut it) = krate.module {
if let ModuleItem(Module { ref mut items, .. }) = *it.kind {
items.extend(synth.impls);
@ -192,16 +216,20 @@ struct BadImplStripper {
}
impl BadImplStripper {
fn keep_item(&self, ty: &Type) -> bool {
fn keep_impl(&self, ty: &Type) -> bool {
if let Generic(_) = ty {
// keep impls made on generics
true
} else if let Some(prim) = ty.primitive_type() {
self.prims.contains(&prim)
} else if let Some(did) = ty.def_id() {
self.items.contains(&did)
self.keep_impl_with_def_id(&did)
} else {
false
}
}
fn keep_impl_with_def_id(&self, did: &DefId) -> bool {
self.items.contains(did)
}
}

View file

@ -0,0 +1,17 @@
// check-pass
// #26207: Ensure `Deref` cycles are properly handled without errors.
#[derive(Copy, Clone)]
struct S;
impl std::ops::Deref for S {
type Target = S;
fn deref(&self) -> &S {
self
}
}
fn main() {
let s: S = *******S;
}

View file

@ -0,0 +1,26 @@
// ignore-tidy-linelength
// #26207: Show all methods reachable via Deref impls, recursing through multiple dereferencing
// levels and across multiple crates.
// @has 'foo/struct.Foo.html'
// @has '-' '//*[@id="deref-methods-PathBuf"]' 'Methods from Deref<Target = PathBuf>'
// @has '-' '//*[@class="impl-items"]//*[@id="method.as_path"]' 'pub fn as_path(&self)'
// @has '-' '//*[@id="deref-methods-Path"]' 'Methods from Deref<Target = Path>'
// @has '-' '//*[@class="impl-items"]//*[@id="method.exists"]' 'pub fn exists(&self)'
// @has '-' '//*[@class="sidebar-title"][@href="#deref-methods-PathBuf"]' 'Methods from Deref<Target=PathBuf>'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.as_path"]' 'as_path'
// @has '-' '//*[@class="sidebar-title"][@href="#deref-methods-Path"]' 'Methods from Deref<Target=Path>'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.exists"]' 'exists'
#![crate_name = "foo"]
use std::ops::Deref;
use std::path::PathBuf;
pub struct Foo(PathBuf);
impl Deref for Foo {
type Target = PathBuf;
fn deref(&self) -> &PathBuf { &self.0 }
}

View file

@ -0,0 +1,42 @@
// ignore-tidy-linelength
// #26207: Show all methods reachable via Deref impls, recursing through multiple dereferencing
// levels if needed.
// @has 'foo/struct.Foo.html'
// @has '-' '//*[@id="deref-methods-Bar"]' 'Methods from Deref<Target = Bar>'
// @has '-' '//*[@class="impl-items"]//*[@id="method.bar"]' 'pub fn bar(&self)'
// @has '-' '//*[@id="deref-methods-Baz"]' 'Methods from Deref<Target = Baz>'
// @has '-' '//*[@class="impl-items"]//*[@id="method.baz"]' 'pub fn baz(&self)'
// @has '-' '//*[@class="sidebar-title"][@href="#deref-methods-Bar"]' 'Methods from Deref<Target=Bar>'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.bar"]' 'bar'
// @has '-' '//*[@class="sidebar-title"][@href="#deref-methods-Baz"]' 'Methods from Deref<Target=Baz>'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.baz"]' 'baz'
#![crate_name = "foo"]
use std::ops::Deref;
pub struct Foo(Bar);
pub struct Bar(Baz);
pub struct Baz;
impl Deref for Foo {
type Target = Bar;
fn deref(&self) -> &Bar { &self.0 }
}
impl Deref for Bar {
type Target = Baz;
fn deref(&self) -> &Baz { &self.0 }
}
impl Bar {
/// This appears under `Foo` methods
pub fn bar(&self) {}
}
impl Baz {
/// This should also appear in `Foo` methods when recursing
pub fn baz(&self) {}
}

View file

@ -1,12 +1,14 @@
// ignore-tidy-linelength
#![crate_name = "foo"]
// @has 'foo/struct.Bar.html'
// @has '-' '//*[@id="deref-methods"]' 'Methods from Deref<Target = FooJ>'
// @has '-' '//*[@id="deref-methods-FooJ"]' 'Methods from Deref<Target = FooJ>'
// @has '-' '//*[@class="impl-items"]//*[@id="method.foo_a"]' 'pub fn foo_a(&self)'
// @has '-' '//*[@class="impl-items"]//*[@id="method.foo_b"]' 'pub fn foo_b(&self)'
// @has '-' '//*[@class="impl-items"]//*[@id="method.foo_c"]' 'pub fn foo_c(&self)'
// @has '-' '//*[@class="impl-items"]//*[@id="method.foo_j"]' 'pub fn foo_j(&self)'
// @has '-' '//*[@class="sidebar-title"]' 'Methods from Deref<Target=FooJ>'
// @has '-' '//*[@class="sidebar-title"][@href="#deref-methods-FooJ"]' 'Methods from Deref<Target=FooJ>'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.foo_a"]' 'foo_a'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.foo_b"]' 'foo_b'
// @has '-' '//*[@class="sidebar-links"]/a[@href="#method.foo_c"]' 'foo_c'