Skip to content

Commit 08abcf4

Browse files
brsonclaude
andcommitted
Resolve trait method links and add method anchor IDs
Fix trait method links (e.g. Hasher::finish) by searching trait definitions in addition to impl blocks when resolving method URLs. Add fallback resolution for cross-crate method links by parsing "Type::method" patterns from link text (139 -> 40 unresolved pages). Add id="method.name" anchors to struct, enum, union, and trait templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 40f99d5 commit 08abcf4

6 files changed

Lines changed: 130 additions & 23 deletions

File tree

crates/rustmax-rustdoc/src/render/item.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ pub fn render_trait(ctx: &RenderContext, item: &RenderableItem) -> AnyResult<Str
338338
let sig = linked.render_function_sig(f, method_name);
339339
let method_links = ctx.resolve_item_links(&trait_item.links, depth);
340340
let info = MethodInfo {
341+
name: method_name.to_string(),
341342
signature: sig,
342343
docs: trait_item.docs.as_ref()
343344
.map(|d| ctx.render_markdown_with_item_links(d, depth, &method_links))
@@ -537,6 +538,7 @@ struct AssocTypeInfo {
537538

538539
#[derive(serde::Serialize)]
539540
struct MethodInfo {
541+
name: String,
540542
signature: String,
541543
docs: String,
542544
}
@@ -572,6 +574,7 @@ fn collect_impls(ctx: &RenderContext, type_id: &rustdoc_types::Id, depth: usize)
572574
let sig = linked.render_function_sig(f, method_name);
573575
let method_links = ctx.resolve_item_links(&method_item.links, depth);
574576
methods.push(MethodInfo {
577+
name: method_name.to_string(),
575578
signature: sig,
576579
docs: method_item.docs.as_ref()
577580
.map(|d| ctx.render_markdown_with_item_links(d, depth, &method_links))

crates/rustmax-rustdoc/src/render/mod.rs

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,14 @@ impl<'a> RenderContext<'a> {
184184
) -> HashMap<String, String> {
185185
let mut resolved = HashMap::new();
186186
for (text, id) in links {
187+
// The links field keys may have backtick wrappers (`` `Builder` ``).
188+
// Strip them since preprocess_shortcut_links removes them from URLs.
189+
let clean_key = text.trim_matches('`').to_string();
187190
if let Some(url) = self.resolve_reexport_url(id, current_depth) {
188-
// The links field keys may have backtick wrappers (`` `Builder` ``).
189-
// Strip them since preprocess_shortcut_links removes them from URLs.
190-
let clean_key = text.trim_matches('`').to_string();
191+
resolved.insert(clean_key, url);
192+
} else if let Some(url) = self.resolve_method_from_link_text(&clean_key, current_depth) {
193+
// Fallback: parse "Type::method" from the link text when the
194+
// method ID isn't in our index (common for cross-crate methods).
191195
resolved.insert(clean_key, url);
192196
}
193197
}
@@ -271,27 +275,81 @@ impl<'a> RenderContext<'a> {
271275
) -> Option<String> {
272276
let method_name = method_item.name.as_deref()?;
273277

274-
// Find the parent type by searching impl blocks.
275-
for (_impl_id, impl_item) in &self.krate.index {
276-
let ItemEnum::Impl(impl_) = &impl_item.inner else {
278+
// Find the parent by searching impl blocks and trait definitions.
279+
for (parent_id, parent_item) in &self.krate.index {
280+
match &parent_item.inner {
281+
ItemEnum::Impl(impl_) => {
282+
if !impl_.items.contains(method_id) {
283+
continue;
284+
}
285+
// Found the impl containing this method.
286+
if let Some(type_id) = get_type_id(&impl_.for_) {
287+
if let Some(type_url) = self.resolve_reexport_url(type_id, current_depth) {
288+
let base_url = type_url.split('#').next().unwrap_or(&type_url);
289+
return Some(format!("{}#method.{}", base_url, method_name));
290+
}
291+
}
292+
return None;
293+
}
294+
ItemEnum::Trait(trait_) => {
295+
if !trait_.items.contains(method_id) {
296+
continue;
297+
}
298+
// Found the trait containing this method.
299+
if let Some(trait_url) = self.resolve_reexport_url(parent_id, current_depth) {
300+
let base_url = trait_url.split('#').next().unwrap_or(&trait_url);
301+
return Some(format!("{}#method.{}", base_url, method_name));
302+
}
303+
return None;
304+
}
305+
_ => continue,
306+
}
307+
}
308+
309+
None
310+
}
311+
312+
/// Resolve a "Type::method" pattern from link text.
313+
///
314+
/// Fallback when the method ID isn't in our index (cross-crate methods).
315+
/// Parses the link text to find the parent type, resolves it, and appends
316+
/// a `#method.name` anchor.
317+
fn resolve_method_from_link_text(
318+
&self,
319+
text: &str,
320+
current_depth: usize,
321+
) -> Option<String> {
322+
// Split "Type::method" or "mod::Type::method" on last "::".
323+
let (type_part, method_name) = text.rsplit_once("::")?;
324+
325+
// Search krate.paths for the type by matching the last path segment,
326+
// preferring types/traits over modules.
327+
let type_name = type_part.rsplit("::").next().unwrap_or(type_part);
328+
let mut best: Option<(String, usize)> = None;
329+
330+
for (id, summary) in &self.krate.paths {
331+
let is_type = matches!(
332+
summary.kind,
333+
ItemKind::Struct | ItemKind::Enum | ItemKind::Trait | ItemKind::Union
334+
);
335+
if !is_type {
277336
continue;
278-
};
279-
if !impl_.items.contains(method_id) {
337+
}
338+
if summary.path.last().map(|s| s.as_str()) != Some(type_name) {
280339
continue;
281340
}
282-
// Found the impl containing this method.
283-
// Get the type ID from the impl's for_ type.
284-
if let Some(type_id) = get_type_id(&impl_.for_) {
285-
if let Some(type_url) = self.resolve_reexport_url(type_id, current_depth) {
286-
// Strip any existing fragment.
287-
let base_url = type_url.split('#').next().unwrap_or(&type_url);
288-
return Some(format!("{}#method.{}", base_url, method_name));
341+
if let Some(type_url) = self.resolve_reexport_url(id, current_depth) {
342+
let base_url = type_url.split('#').next().unwrap_or(&type_url);
343+
let url = format!("{}#method.{}", base_url, method_name);
344+
let path_len = summary.path.len();
345+
// Prefer shorter paths (more likely the canonical public location).
346+
if best.as_ref().map_or(true, |(_, len)| path_len < *len) {
347+
best = Some((url, path_len));
289348
}
290349
}
291-
break;
292350
}
293351

294-
None
352+
best.map(|(url, _)| url)
295353
}
296354

297355
/// Find a re-export URL for a local item by checking if it appears as a
@@ -503,6 +561,9 @@ mod tests {
503561
const ENUM_DEF: u32 = 7;
504562
const OK_VAR: u32 = 8;
505563
const ERR_VAR: u32 = 9;
564+
const TRAIT_DEF: u32 = 10;
565+
const TRAIT_METHOD: u32 = 11;
566+
const TRAIT_USE: u32 = 12;
506567

507568
/// Helper to build a minimal Item.
508569
fn item(id: u32, name: &str, vis: Visibility, inner: ItemEnum) -> (Id, Item) {
@@ -574,6 +635,18 @@ mod tests {
574635
})
575636
}
576637

638+
fn empty_trait(method_ids: Vec<u32>) -> ItemEnum {
639+
ItemEnum::Trait(Trait {
640+
is_auto: false,
641+
is_unsafe: false,
642+
is_dyn_compatible: true,
643+
items: method_ids.into_iter().map(Id).collect(),
644+
generics: Generics { params: vec![], where_predicates: vec![] },
645+
bounds: vec![],
646+
implementations: vec![],
647+
})
648+
}
649+
577650
fn path_entry(crate_id: u32, path: &[&str], kind: ItemKind) -> ItemSummary {
578651
ItemSummary {
579652
crate_id,
@@ -617,7 +690,7 @@ mod tests {
617690

618691
// thread module.
619692
let (id, i) = item(THREAD_MOD, "thread", Visibility::Public,
620-
module(false, vec![JH_USE, SPAWN_USE, ENUM_USE]));
693+
module(false, vec![JH_USE, SPAWN_USE, ENUM_USE, TRAIT_USE]));
621694
index.insert(id.clone(), i);
622695
paths.insert(id, path_entry(0, &["mycrate", "thread"], ItemKind::Module));
623696

@@ -666,6 +739,22 @@ mod tests {
666739
paths.insert(id, path_entry(0,
667740
&["mycrate", "_priv", "MyEnum", "Err"], ItemKind::Variant));
668741

742+
// MyTrait definition (in private module) with one method.
743+
let (id, i) = item(TRAIT_DEF, "MyTrait", Visibility::Public,
744+
empty_trait(vec![TRAIT_METHOD]));
745+
index.insert(id.clone(), i);
746+
paths.insert(id, path_entry(0,
747+
&["mycrate", "_priv", "MyTrait"], ItemKind::Trait));
748+
749+
// Trait method.
750+
let (id, i) = item(TRAIT_METHOD, "do_stuff", Visibility::Public, empty_fn());
751+
index.insert(id, i);
752+
753+
// Re-export: MyTrait.
754+
let (id, i) = item(TRAIT_USE, "MyTrait", Visibility::Public,
755+
use_item("mycrate::_priv::MyTrait", "MyTrait", TRAIT_DEF));
756+
index.insert(id, i);
757+
669758
krate
670759
}
671760

@@ -732,6 +821,21 @@ mod tests {
732821
);
733822
}
734823

824+
#[test]
825+
fn test_resolve_trait_method_url() {
826+
let krate = test_crate();
827+
let config = test_config();
828+
let ctx = RenderContext::new(&krate, &config).unwrap();
829+
830+
// Trait method do_stuff is defined inside MyTrait. It should resolve
831+
// to the trait page at the re-export location with a #method anchor.
832+
let url = ctx.resolve_reexport_url(&Id(TRAIT_METHOD), 2);
833+
assert_eq!(
834+
url,
835+
Some("../../mycrate/thread/trait.MyTrait.html#method.do_stuff".to_string()),
836+
);
837+
}
838+
735839
#[test]
736840
fn test_build_breadcrumbs_item_page() {
737841
// Item at std::thread::spawn, depth=2 (file at std/thread/fn.spawn.html).

crates/rustmax-rustdoc/src/templates/enum.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ <h3><code>{{ impl_block.header | safe }}</code></h3>
8383
{% if impl_block.methods | length > 0 %}
8484
<dl>
8585
{% for method in impl_block.methods %}
86-
<dt><pre><code>{{ method.signature | safe }}</code></pre></dt>
86+
<dt id="method.{{ method.name }}"><pre><code>{{ method.signature | safe }}</code></pre></dt>
8787
{% if method.docs %}
8888
<dd>{{ method.docs | safe }}</dd>
8989
{% endif %}

crates/rustmax-rustdoc/src/templates/struct.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ <h3><code>{{ impl_block.header | safe }}</code></h3>
8181
{% if impl_block.methods | length > 0 %}
8282
<dl>
8383
{% for method in impl_block.methods %}
84-
<dt><pre><code>{{ method.signature | safe }}</code></pre></dt>
84+
<dt id="method.{{ method.name }}"><pre><code>{{ method.signature | safe }}</code></pre></dt>
8585
{% if method.docs %}
8686
<dd>{{ method.docs | safe }}</dd>
8787
{% endif %}

crates/rustmax-rustdoc/src/templates/trait.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ <h2>Associated Types</h2>
7777
<h2>Required Methods</h2>
7878
<dl>
7979
{% for method in required_methods %}
80-
<dt><pre><code>{{ method.signature | safe }}</code></pre></dt>
80+
<dt id="method.{{ method.name }}"><pre><code>{{ method.signature | safe }}</code></pre></dt>
8181
{% if method.docs %}
8282
<dd>{{ method.docs | safe }}</dd>
8383
{% endif %}
@@ -91,7 +91,7 @@ <h2>Required Methods</h2>
9191
<h2>Provided Methods</h2>
9292
<dl>
9393
{% for method in provided_methods %}
94-
<dt><pre><code>{{ method.signature | safe }}</code></pre></dt>
94+
<dt id="method.{{ method.name }}"><pre><code>{{ method.signature | safe }}</code></pre></dt>
9595
{% if method.docs %}
9696
<dd>{{ method.docs | safe }}</dd>
9797
{% endif %}

crates/rustmax-rustdoc/src/templates/union.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ <h3><code>{{ impl_block.header | safe }}</code></h3>
8181
{% if impl_block.methods | length > 0 %}
8282
<dl>
8383
{% for method in impl_block.methods %}
84-
<dt><pre><code>{{ method.signature | safe }}</code></pre></dt>
84+
<dt id="method.{{ method.name }}"><pre><code>{{ method.signature | safe }}</code></pre></dt>
8585
{% if method.docs %}
8686
<dd>{{ method.docs | safe }}</dd>
8787
{% endif %}

0 commit comments

Comments
 (0)