Skip to main content

ferritin_common/
doc_ref.rs

1use crate::{
2    Navigator, RustdocData, navigator::parse_docsrs_url, rustdoc_data::kind_discriminator,
3};
4use fieldwork::Fieldwork;
5use rustdoc_types::{
6    ExternalCrate, Id, Item, ItemEnum, ItemKind, ItemSummary, MacroKind, ProcMacro, Use,
7};
8
9/// A lightweight, `Copy` reference to a parent item set during tree traversal.
10///
11/// Stored on [`DocRef`] to enable [`DocRef::discriminated_path`] for items absent from
12/// rustdoc's `paths` map (e.g. inherent methods; rust-lang/rust#152511). One level is
13/// sufficient because only impl-block items are orphaned, and their parents (structs,
14/// enums, traits) always have an `ItemSummary`.
15#[derive(Copy, Clone, Debug)]
16pub(crate) struct ParentRef<'a> {
17    pub(crate) crate_docs: &'a RustdocData,
18    pub(crate) item: &'a Item,
19    /// The name override from the parent's [`DocRef`], if it was set (e.g. by a re-export).
20    pub(crate) name: Option<&'a str>,
21}
22
23impl<'a> From<DocRef<'a, Item>> for ParentRef<'a> {
24    fn from(d: DocRef<'a, Item>) -> Self {
25        ParentRef {
26            crate_docs: d.crate_docs,
27            item: d.item,
28            name: d.name,
29        }
30    }
31}
32use semver::VersionReq;
33use std::{
34    fmt::{self, Debug, Display, Formatter},
35    hash::{Hash, Hasher},
36    ops::Deref,
37};
38
39#[derive(Fieldwork)]
40#[fieldwork(get, option_set_some)]
41pub struct DocRef<'a, T> {
42    crate_docs: &'a RustdocData,
43    item: &'a T,
44    navigator: &'a Navigator,
45
46    #[field(get = false, with, set)]
47    name: Option<&'a str>,
48
49    /// Parent item set during tree traversal; used by [`DocRef::discriminated_path`] as a
50    /// fallback for items absent from rustdoc's `paths` map (rust-lang/rust#152511).
51    #[field(get = false, with(vis = "pub(crate)", option_set_some, into))]
52    parent: Option<ParentRef<'a>>,
53}
54
55// Equality based on item pointer and crate provenance
56impl<'a, T> PartialEq for DocRef<'a, T> {
57    fn eq(&self, other: &Self) -> bool {
58        std::ptr::eq(self.item, other.item) && std::ptr::eq(self.crate_docs, other.crate_docs)
59    }
60}
61
62impl<'a, T> Eq for DocRef<'a, T> {}
63
64impl Hash for DocRef<'_, Item> {
65    fn hash<H: Hasher>(&self, state: &mut H) {
66        self.crate_docs.name().hash(state);
67        self.id.hash(state);
68    }
69}
70
71impl<'a, T> From<&DocRef<'a, T>> for &'a RustdocData {
72    fn from(value: &DocRef<'a, T>) -> Self {
73        value.crate_docs
74    }
75}
76impl<'a, T> From<DocRef<'a, T>> for &'a RustdocData {
77    fn from(value: DocRef<'a, T>) -> Self {
78        value.crate_docs
79    }
80}
81
82impl<'a, T> Deref for DocRef<'a, T> {
83    type Target = T;
84
85    fn deref(&self) -> &Self::Target {
86        self.item
87    }
88}
89
90impl<'a, T> DocRef<'a, T> {
91    pub fn build_ref<U>(&self, inner: &'a U) -> DocRef<'a, U> {
92        DocRef::new(self.navigator, self.crate_docs, inner)
93    }
94
95    pub fn get_path(&self, id: Id) -> Option<DocRef<'a, Item>> {
96        self.crate_docs.get_path(self.navigator, id)
97    }
98}
99
100impl<'a> DocRef<'a, Item> {
101    pub fn name(&self) -> Option<&'a str> {
102        self.name
103            .or(self.item.name.as_deref())
104            .or(self.summary().and_then(|x| x.path.last().map(|y| &**y)))
105    }
106
107    pub fn inner(&self) -> &'a ItemEnum {
108        &self.item.inner
109    }
110
111    pub fn path(&self) -> Option<Path<'a>> {
112        self.crate_docs().path(&self.id)
113    }
114
115    pub fn summary(&self) -> Option<&'a ItemSummary> {
116        self.crate_docs().paths.get(&self.id)
117    }
118
119    pub fn find_child(&self, child_name: &str) -> Option<DocRef<'a, Item>> {
120        self.child_items()
121            .find(|c| c.name().is_some_and(|n| n == child_name))
122    }
123
124    pub fn find_by_path<'b>(
125        &self,
126        mut iter: impl Iterator<Item = &'b String>,
127    ) -> Option<DocRef<'a, Item>> {
128        let Some(next) = iter.next() else {
129            return Some(*self);
130        };
131
132        for child in self.child_items() {
133            if let Some(name) = child.name()
134                && name == next
135            {
136                return child.find_by_path(iter);
137            }
138        }
139
140        None
141    }
142
143    /// Returns the fully-qualified, kind-discriminated path for this item, suitable for
144    /// round-tripping through `Navigator::resolve_path`.
145    ///
146    /// For example, a `Vec` struct in `std::vec` returns `"std::vec::struct@Vec"`, and the
147    /// `vec` module itself returns `"std::mod@vec"`. The crate name is included as the first
148    /// segment; the discriminator (`kind@`) appears only on the final segment.
149    ///
150    /// Uses `crate_docs().name()` (the Navigator's canonical crate name) rather than
151    /// `ItemSummary::path[0]` (which rustdoc normalizes to underscores) so that the
152    /// generated path round-trips correctly through `Navigator::resolve_path`.
153    ///
154    /// Returns `None` if the item has no `ItemSummary` entry in the crate's paths map.
155    pub fn discriminated_path(&self) -> Option<String> {
156        if let Some(summary) = self.summary() {
157            // Fast path: use the ItemSummary path directly.
158            // path[0] is the crate name as rustdoc sees it (underscored); use the Navigator's
159            // canonical name instead so the result can be fed back into resolve_path.
160            let path = &summary.path;
161            let tail = path.get(1..)?;
162            let disc = kind_discriminator(self.kind());
163            let crate_name = self.crate_docs().name();
164            return match tail {
165                [] => Some(format!("{crate_name}::{disc}@{}", path[0])),
166                [.., last] => {
167                    let prefix = tail[..tail.len() - 1].join("::");
168                    if prefix.is_empty() {
169                        Some(format!("{crate_name}::{disc}@{last}"))
170                    } else {
171                        Some(format!("{crate_name}::{prefix}::{disc}@{last}"))
172                    }
173                }
174            };
175        }
176
177        // Fallback for items absent from rustdoc's paths map (e.g. inherent methods;
178        // rust-lang/rust#152511). Requires a parent set during tree traversal.
179        let parent_ref = self.parent?;
180        let disc = kind_discriminator(self.kind());
181        let name = self.item.name.as_deref()?;
182
183        // Prefer a path without a discriminator on the parent segment (simpler output).
184        // The unqualified key is only present in path_to_id when there is no collision at
185        // that path, so its presence is a reliable signal that we can omit the discriminator.
186        if let Some(parent_summary) = parent_ref.crate_docs.paths.get(&parent_ref.item.id) {
187            if let Some(tail) = parent_summary.path.get(1..) {
188                let parent_key = tail.join("::");
189                if parent_ref.crate_docs.path_to_id.contains_key(&parent_key) {
190                    let crate_name = parent_ref.crate_docs.name();
191                    let parent_path = if parent_key.is_empty() {
192                        crate_name.to_string()
193                    } else {
194                        format!("{crate_name}::{parent_key}")
195                    };
196                    return Some(format!("{parent_path}::{disc}@{name}"));
197                }
198            }
199        }
200
201        // Collision at the parent level: fall back to the fully-discriminated parent path.
202        let parent = DocRef::new(self.navigator, parent_ref.crate_docs, parent_ref.item);
203        let parent = match parent_ref.name {
204            Some(n) => parent.with_name(n),
205            None => parent,
206        };
207        let parent_path = parent.discriminated_path()?;
208        Some(format!("{parent_path}::{disc}@{name}"))
209    }
210
211    pub fn kind(&self) -> ItemKind {
212        match self.item.inner {
213            ItemEnum::Module(_) => ItemKind::Module,
214            ItemEnum::ExternCrate { .. } => ItemKind::ExternCrate,
215            ItemEnum::Use(_) => ItemKind::Use,
216            ItemEnum::Union(_) => ItemKind::Union,
217            ItemEnum::Struct(_) => ItemKind::Struct,
218            ItemEnum::StructField(_) => ItemKind::StructField,
219            ItemEnum::Enum(_) => ItemKind::Enum,
220            ItemEnum::Variant(_) => ItemKind::Variant,
221            ItemEnum::Function(_) => ItemKind::Function,
222            ItemEnum::Trait(_) => ItemKind::Trait,
223            ItemEnum::TraitAlias(_) => ItemKind::TraitAlias,
224            ItemEnum::Impl(_) => ItemKind::Impl,
225            ItemEnum::TypeAlias(_) => ItemKind::TypeAlias,
226            ItemEnum::Constant { .. } => ItemKind::Constant,
227            ItemEnum::Static(_) => ItemKind::Static,
228            ItemEnum::ExternType => ItemKind::ExternType,
229            ItemEnum::ProcMacro(ProcMacro {
230                kind: MacroKind::Attr,
231                ..
232            }) => ItemKind::ProcAttribute,
233            ItemEnum::ProcMacro(ProcMacro {
234                kind: MacroKind::Derive,
235                ..
236            }) => ItemKind::ProcDerive,
237            ItemEnum::Macro(_)
238            | ItemEnum::ProcMacro(ProcMacro {
239                kind: MacroKind::Bang,
240                ..
241            }) => ItemKind::Macro,
242            ItemEnum::Primitive(_) => ItemKind::Primitive,
243            ItemEnum::AssocConst { .. } => ItemKind::AssocConst,
244            ItemEnum::AssocType { .. } => ItemKind::AssocType,
245        }
246    }
247}
248
249impl<'a, T> Clone for DocRef<'a, T> {
250    fn clone(&self) -> Self {
251        *self
252    }
253}
254
255impl<'a, T> Copy for DocRef<'a, T> {}
256
257impl<'a, T: Debug> Debug for DocRef<'a, T> {
258    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
259        f.debug_struct("DocRef")
260            .field("crate_docs", &self.crate_docs)
261            .field("item", &self.item)
262            .finish_non_exhaustive()
263    }
264}
265
266impl<'a, T> DocRef<'a, T> {
267    pub(crate) fn new(
268        navigator: &'a Navigator,
269        crate_docs: impl Into<&'a RustdocData>,
270        item: &'a T,
271    ) -> Self {
272        let crate_docs = crate_docs.into();
273        Self {
274            navigator,
275            crate_docs,
276            item,
277            name: None,
278            parent: None,
279        }
280    }
281
282    pub fn get(&self, id: &Id) -> Option<DocRef<'a, Item>> {
283        self.crate_docs.get(self.navigator, id)
284    }
285}
286
287impl<'a> DocRef<'a, Use> {
288    pub fn use_name(self) -> &'a str {
289        self.name.unwrap_or(&self.item.name)
290    }
291}
292
293impl<'a> DocRef<'a, ItemSummary> {
294    /// Get the external crate this item summary refers to, if any.
295    /// Returns None if crate_id == 0 (same crate).
296    pub fn external_crate(&self) -> Option<DocRef<'a, ExternalCrate>> {
297        if self.crate_id == 0 {
298            return None;
299        }
300
301        let external = self.crate_docs().external_crates.get(&self.crate_id)?;
302        Some(self.build_ref(external))
303    }
304}
305
306impl<'a> DocRef<'a, ExternalCrate> {
307    /// Get the canonical name of this external crate.
308    /// Parses html_root_url if available, falls back to the name field.
309    pub fn crate_name(&self) -> &'a str {
310        if let Some(url) = &self.item.html_root_url {
311            if let Some((name, _)) = parse_docsrs_url(url) {
312                return name;
313            }
314        }
315        &self.item.name
316    }
317
318    /// Load the RustdocData for this external crate.
319    pub fn load(&self) -> Option<&'a RustdocData> {
320        let name = self.crate_name();
321        let version_req = if let Some(url) = &self.item.html_root_url {
322            parse_docsrs_url(url)
323                .and_then(|(_, version)| VersionReq::parse(&format!("={version}")).ok())
324                .unwrap_or(VersionReq::STAR)
325        } else {
326            VersionReq::STAR
327        };
328
329        self.navigator().load_crate(name, &version_req)
330    }
331}
332
333#[derive(Debug)]
334pub struct Path<'a>(&'a [String]);
335
336impl<'a> From<&'a ItemSummary> for Path<'a> {
337    fn from(value: &'a ItemSummary) -> Self {
338        Self(&value.path)
339    }
340}
341
342impl<'a> IntoIterator for Path<'a> {
343    type Item = &'a str;
344
345    type IntoIter = Box<dyn Iterator<Item = Self::Item> + 'a>;
346
347    fn into_iter(self) -> Self::IntoIter {
348        Box::new(self.0.iter().map(|x| &**x))
349    }
350}
351
352impl Display for Path<'_> {
353    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
354        for (i, segment) in self.0.iter().enumerate() {
355            if i > 0 {
356                f.write_str("::")?;
357            }
358            f.write_str(segment)?;
359        }
360        Ok(())
361    }
362}
363
364// Compile-time thread-safety assertions for DocRef
365//
366// DocRef holds references (&'a T, &'a Navigator, &'a RustdocData) which are Send
367// when the referenced types are Sync. This is critical for the threading model:
368// DocRef can be sent between threads in scoped thread scenarios.
369#[allow(dead_code)]
370const _: () = {
371    const fn assert_send<T: Send>() {}
372    const fn assert_sync<T: Sync>() {}
373
374    // DocRef<'a, Item> must be Send (can cross thread boundaries in scoped threads)
375    const fn check_doc_ref_send() {
376        assert_send::<DocRef<'_, rustdoc_types::Item>>();
377    }
378
379    // DocRef<'a, Item> must be Sync (multiple threads can hold &DocRef safely)
380    const fn check_doc_ref_sync() {
381        assert_sync::<DocRef<'_, rustdoc_types::Item>>();
382    }
383};