Skip to main content

ferritin_common/
rustdoc_data.rs

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