1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
use fieldwork::Fieldwork;
use rustdoc_types::{Crate, ExternalCrate, Id, Item, ItemKind};
use semver::{Version, VersionReq};
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::ops::Deref;
use std::path::PathBuf;
use crate::CrateProvenance;
use crate::doc_ref::{self, DocRef};
use crate::navigator::{Navigator, parse_docsrs_url};
/// Wrapper around rustdoc JSON data that provides convenient query methods
#[derive(Clone, Fieldwork, PartialEq, Eq)]
#[fieldwork(get, rename_predicates)]
pub struct RustdocData {
pub(crate) crate_data: Crate,
pub(crate) name: String,
pub(crate) provenance: CrateProvenance,
pub(crate) fs_path: PathBuf,
pub(crate) version: Option<Version>,
/// Reverse index from path string (excluding crate name) to `Id`, for local items.
///
/// Populated by [`RustdocData::build_path_index`] before crate insertion into Navigator.
/// Used as a fallback in `Navigator::resolve_path` when tree traversal fails (e.g. when
/// the path passes through a private module not visible in the public item tree).
///
/// Contains two kinds of entries per item:
/// - A kind-qualified key: `"mod1::mod@name"` or `"mod1::fn@name"` — always present,
/// allows users to explicitly request a specific kind when names collide.
/// - An unqualified key: `"mod1::name"` — present only when no other item of a different
/// kind shares this path (i.e. unambiguous).
#[field = false]
pub(crate) path_to_id: HashMap<String, Id>,
}
impl Debug for RustdocData {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("RustdocData")
.field("name", &self.name)
.field("crate_type", &self.provenance)
.field("fs_path", &self.fs_path)
.field("version", &self.version)
.finish()
}
}
impl Deref for RustdocData {
type Target = Crate;
fn deref(&self) -> &Self::Target {
&self.crate_data
}
}
impl RustdocData {
pub(crate) fn get<'a>(&'a self, navigator: &'a Navigator, id: &Id) -> Option<DocRef<'a, Item>> {
let item = self.crate_data.index.get(id)?;
Some(DocRef::new(navigator, self, item))
}
pub fn path<'a>(&'a self, id: &Id) -> Option<doc_ref::Path<'a>> {
self.paths.get(id).map(|summary| summary.into())
}
pub fn root_item<'a>(&'a self, navigator: &'a Navigator) -> DocRef<'a, Item> {
DocRef::new(navigator, self, &self.index[&self.root])
}
pub fn traverse_to_crate_by_id<'a>(
&'a self,
navigator: &'a Navigator,
id: u32,
) -> Option<&'a RustdocData> {
if id == 0 {
//special case: 0 is not in external crates, and it always means "this crate"
return Some(self);
}
let ExternalCrate {
name,
html_root_url,
..
} = self.external_crates.get(&id)?;
let (name, version_req) = html_root_url.as_deref().and_then(parse_docsrs_url).map_or(
(&**name, VersionReq::STAR),
|(name, version)| {
let version_req =
VersionReq::parse(&format!("={version}")).unwrap_or(VersionReq::STAR);
(name, version_req)
},
);
navigator.load_crate(name, &version_req)
}
/// Build the reverse path index from `paths`, for use by `Navigator::resolve_path`.
///
/// Indexes local items (`crate_id == 0`) by their path string (excluding the crate name
/// prefix). For example, an item at `["my_crate", "private", "MyStruct"]` gets:
///
/// - A kind-qualified entry: `"private::struct@MyStruct"` → Id (always)
/// - An unqualified entry: `"private::MyStruct"` → Id (only if no collision at that path)
pub(crate) fn build_path_index(&mut self) {
// Collect all local items grouped by their unqualified path.
let mut by_unqualified: HashMap<String, Vec<(Id, ItemKind)>> = HashMap::new();
for (id, summary) in &self.crate_data.paths {
if summary.crate_id != 0 {
continue;
}
let Some(tail) = summary.path.get(1..) else {
continue;
};
if tail.is_empty() {
continue;
}
by_unqualified
.entry(tail.join("::"))
.or_default()
.push((*id, summary.kind));
}
let mut map = HashMap::new();
for (unqualified, items) in &by_unqualified {
// Split into prefix and last segment name so the discriminator goes on the
// final segment only: e.g. "mod1::mod2::fn@name" not "fn@mod1::mod2::name".
let (prefix, last_name) = match unqualified.rfind("::") {
Some(sep) => (&unqualified[..sep + 2], &unqualified[sep + 2..]),
None => ("", unqualified.as_str()),
};
// Always insert a kind-qualified entry for each item.
for (id, kind) in items {
let qualified = format!("{prefix}{}@{last_name}", kind_discriminator(*kind));
map.insert(qualified, *id);
}
// Insert the unqualified entry only when it is unambiguous (exactly one item).
if items.len() == 1 {
map.insert(unqualified.clone(), items[0].0);
}
}
self.path_to_id = map;
}
}
/// Returns the rustdoc discriminator prefix for an item kind, e.g. `"mod"` for `Module`.
///
/// Matches rustdoc's intra-doc link disambiguator syntax. Notably:
/// - `"tyalias"` for `TypeAlias` (rustdoc uses `tyalias@` / `typealias@`)
/// - `"type"` for `AssocType` (rustdoc uses `type@` for associated types)
/// - `"fn"` for both functions and methods
pub(crate) fn kind_discriminator(kind: ItemKind) -> &'static str {
match kind {
ItemKind::Module => "mod",
ItemKind::Struct => "struct",
ItemKind::Enum => "enum",
ItemKind::Union => "union",
ItemKind::Trait => "trait",
ItemKind::TraitAlias => "traitalias",
ItemKind::Function => "fn",
ItemKind::TypeAlias => "tyalias",
ItemKind::AssocType => "type",
ItemKind::Constant | ItemKind::AssocConst => "const",
ItemKind::Static => "static",
ItemKind::Macro => "macro",
ItemKind::ProcAttribute => "attr",
ItemKind::ProcDerive => "derive",
ItemKind::Primitive => "prim",
ItemKind::Variant => "variant",
ItemKind::StructField => "field",
ItemKind::Keyword => "keyword",
ItemKind::Attribute => "attribute",
ItemKind::ExternCrate | ItemKind::Use | ItemKind::Impl | ItemKind::ExternType => "item",
}
}