Skip to main content

ferritin_common/
resolver.rs

1//! Resolver — per-query navigation engine over a [`Navigator`].
2//!
3//! A [`Resolver`] holds a borrow on a [`Navigator`] (the long-lived data store
4//! of loaded crates) plus the mutable state of a single navigation: a frame
5//! stack used both for cycle detection and for tracking the path traversed.
6//!
7//! # The cycle problem
8//!
9//! Path-string resolution can cycle through `Use` re-exports across crate
10//! boundaries. The classic case is mutual re-exports between `askama` and
11//! `askama_macros`: resolving a Use's source path lands on another Use,
12//! whose source path lands on yet another Use, eventually back at the first.
13//! The cycle never repeats the same `(parent, target)` pair within a single
14//! [`Resolver::find_named`] call, so a guard local to one name lookup misses
15//! it. The cycle does, however, repeat the same frame across different
16//! lookups in the same chain — which is what the resolver-wide stack catches.
17//!
18//! # Frame discipline
19//!
20//! Two kinds of frame are pushed:
21//! - **`Some(target)`** while looking up name `target` in some parent module.
22//! - **`None`** while following a `Use` to its target.
23//!
24//! Pushing a frame already on the stack (same item key, same segment) returns
25//! `None` from the scoped `with_pushed` guard. The frame is popped when the
26//! guard's scope ends, including after a successful resolution — the stack
27//! drains to empty between top-level calls.
28//!
29//! # Semantic path
30//!
31//! [`Resolver::current_path`] reads the segments off the stack in order, which
32//! is the actual public route walked to reach the current item. This is more
33//! accurate than `DocRef::discriminated_path` for items reached via
34//! re-exports, where the canonical path may go through private modules.
35
36use crate::doc_ref::ParentRef;
37use crate::iterators::{LazyChild, LazyChildren};
38use crate::string_utils::case_aware_jaro_winkler;
39use crate::{DocRef, Navigator, RustdocData};
40use fieldwork::Fieldwork;
41use rustdoc_types::{Id, Item, ItemEnum, ItemKind, Use};
42use semver::VersionReq;
43
44/// Suggestion produced when path resolution fails — a near-miss path the user
45/// might have meant.
46#[derive(Fieldwork)]
47#[fieldwork(get)]
48pub struct Suggestion<'a> {
49    path: String,
50    item: Option<DocRef<'a, Item>>,
51    score: f64,
52}
53
54/// Stable identity for any borrowed item, used as the cycle-detection key.
55///
56/// Both addresses are stable for the lifetime of the `Navigator`: `crate_docs`
57/// because `working_set` is a `FrozenMap` that never moves entries; the inner
58/// pointer because each `Item` and each `Use` lives at a fixed location inside
59/// the rustdoc `Crate.index` map. We compare addresses as `usize` so the key
60/// stays `Send + Sync` and `Hash`able without unsafe.
61#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
62pub(crate) struct ItemKey {
63    crate_docs: usize,
64    item: usize,
65}
66
67impl<'a, T> From<DocRef<'a, T>> for ItemKey {
68    fn from(d: DocRef<'a, T>) -> Self {
69        Self {
70            crate_docs: d.crate_docs() as *const RustdocData as usize,
71            item: d.item() as *const T as usize,
72        }
73    }
74}
75
76#[derive(Clone, PartialEq, Eq, Debug)]
77struct Frame {
78    item: ItemKey,
79    /// `None` while following a `Use`; `Some(name)` while looking up `name`
80    /// inside `item`. Owned so the frame can outlive the caller's borrow of
81    /// the segment string (e.g. a slice of the user's input `path`).
82    segment: Option<String>,
83}
84
85/// A navigation engine over a [`Navigator`].
86///
87/// Construct one per query (e.g. one per top-level command in a `Request`).
88/// The frame stack drains to empty between top-level methods, so a single
89/// `Resolver` is reusable across many lookups in the same operation.
90///
91/// Methods that walk through items take `&mut self` because resolution
92/// mutates the frame stack. Returned `DocRef`s borrow from the underlying
93/// `Navigator`, so they outlive the `Resolver`.
94pub struct Resolver<'a> {
95    navigator: &'a Navigator,
96    stack: Vec<Frame>,
97}
98
99impl<'a> Resolver<'a> {
100    pub fn new(navigator: &'a Navigator) -> Self {
101        Self {
102            navigator,
103            stack: Vec::new(),
104        }
105    }
106
107    pub fn navigator(&self) -> &'a Navigator {
108        self.navigator
109    }
110
111    /// Segments traversed in the current resolution chain. Empty between
112    /// top-level calls.
113    pub fn current_path(&self) -> impl Iterator<Item = &str> + '_ {
114        self.stack.iter().filter_map(|f| f.segment.as_deref())
115    }
116
117    /// Push a frame, run `f`, pop. Returns `None` if the frame is already on
118    /// the stack (cycle).
119    fn with_pushed<R>(
120        &mut self,
121        item: ItemKey,
122        segment: Option<&str>,
123        f: impl FnOnce(&mut Self) -> R,
124    ) -> Option<R> {
125        let frame = Frame {
126            item,
127            segment: segment.map(str::to_owned),
128        };
129        if self.stack.contains(&frame) {
130            log::trace!("cycle: {frame:?}");
131            return None;
132        }
133        self.stack.push(frame);
134        let r = f(self);
135        self.stack.pop();
136        Some(r)
137    }
138}
139
140// ---------- top-level path resolution ----------
141
142impl<'a> Resolver<'a> {
143    /// Resolve a path like `"std::vec::Vec"` or `"tokio@1::runtime::Runtime"`.
144    ///
145    /// Primary string entrypoint for any user-supplied crate or item path.
146    pub fn resolve_path(
147        &mut self,
148        mut path: &str,
149        suggestions: &mut Vec<Suggestion<'a>>,
150    ) -> Option<DocRef<'a, Item>> {
151        if let Some(p) = path.strip_prefix("::") {
152            path = p;
153        }
154
155        let (crate_specifier, path_start_index) = if let Some(first_scope) = path.find("::") {
156            (&path[..first_scope], Some(first_scope + 2))
157        } else {
158            (path, None)
159        };
160
161        let (crate_name, version_req) = if let Some(at) = crate_specifier.find("@") {
162            (
163                &crate_specifier[..at],
164                VersionReq::parse(&crate_specifier[at + 1..]).unwrap_or(VersionReq::STAR),
165            )
166        } else {
167            (crate_specifier, VersionReq::STAR)
168        };
169
170        let Some(crate_data) = self.navigator.load_crate(crate_name, &version_req) else {
171            suggestions.extend(self.navigator.list_available_crates().map(|crate_info| {
172                Suggestion {
173                    path: crate_info.name().to_string(),
174                    item: None,
175                    score: case_aware_jaro_winkler(crate_info.name(), crate_name),
176                }
177            }));
178            return None;
179        };
180
181        let item = crate_data.get(self.navigator, &crate_data.root)?;
182        let Some(path_start_index) = path_start_index else {
183            return Some(item);
184        };
185
186        if let Some(item) = self.find_children_recursive(item, path, path_start_index, suggestions)
187        {
188            return Some(item);
189        }
190
191        let suffix = &path[path_start_index..];
192
193        // Fallback: rustdoc paths map. Catches paths through private modules
194        // not visible in the public item tree.
195        if let Some(item) = crate_data
196            .path_to_id
197            .get(suffix)
198            .and_then(|id| crate_data.index.get(id))
199            .map(|item| DocRef::new(self.navigator, crate_data, item))
200        {
201            return Some(item);
202        }
203
204        // Second fallback: parent in paths map but child orphaned (impl-block
205        // items missing from rustdoc's paths). Strip the last segment and
206        // walk into the child.
207        if let Some(sep) = suffix.rfind("::") {
208            let parent_suffix = &suffix[..sep];
209            let child_start = path_start_index + sep + 2;
210            if let Some(parent_id) = crate_data.path_to_id.get(parent_suffix)
211                && let Some(parent_item) = crate_data.index.get(parent_id)
212            {
213                let parent_ref = DocRef::new(self.navigator, crate_data, parent_item);
214                return self.find_children_recursive(parent_ref, path, child_start, suggestions);
215            }
216        }
217
218        None
219    }
220
221    /// Walk path segments starting at `index`, looking up each segment as a
222    /// child of the current item.
223    fn find_children_recursive(
224        &mut self,
225        item: DocRef<'a, Item>,
226        path: &str,
227        index: usize,
228        suggestions: &mut Vec<Suggestion<'a>>,
229    ) -> Option<DocRef<'a, Item>> {
230        let remaining = &path[path.len().min(index)..];
231        if remaining.is_empty() {
232            return Some(item);
233        }
234        let segment_end = remaining
235            .find("::")
236            .map(|x| index + x)
237            .unwrap_or(path.len());
238        let segment = &path[index..segment_end];
239        let next_segment_start = path.len().min(segment_end + 2);
240
241        let (kind_filter, segment_name) = parse_discriminated_segment(segment);
242
243        log::trace!(
244            "🔎 searching for {segment_name} (kind={kind_filter:?}) in {} ({:?}) (remaining {})",
245            &path[..index],
246            item.kind(),
247            &path[next_segment_start..]
248        );
249
250        if let Some(found) = self.find_named(item, segment_name, |resolver, candidate| {
251            if !kind_filter.is_none_or(|k| candidate.kind() == k) {
252                return None;
253            }
254            resolver.find_children_recursive(candidate, path, next_segment_start, suggestions)
255        }) {
256            return Some(found);
257        }
258
259        suggestions.extend(self.generate_suggestions(item, path, index));
260        None
261    }
262
263    fn generate_suggestions(
264        &mut self,
265        item: DocRef<'a, Item>,
266        path: &str,
267        index: usize,
268    ) -> Vec<Suggestion<'a>> {
269        // Eager-collect — walking children involves Use resolution that needs
270        // &mut self, so we can't return a borrowing iterator.
271        let prefix = path[..index].to_owned();
272        let path_owned = path.to_owned();
273        let mut candidates = self.children(item);
274        // Trait-declared associated items aren't part of `children()` (see
275        // `find_named_dyn`); include them so a mistyped member like
276        // `Deref::derefx` suggests the real members rather than only siblings.
277        if let ItemEnum::Trait(trait_) = item.inner() {
278            candidates.extend(trait_.items.iter().filter_map(|id| item.get(id)));
279        }
280        candidates
281            .into_iter()
282            .filter_map(move |item| {
283                item.name().and_then(|name| {
284                    let full_path = format!("{prefix}{name}");
285                    if path_owned.starts_with(&full_path) {
286                        None
287                    } else {
288                        let score = case_aware_jaro_winkler(&path_owned, &full_path);
289                        Some(Suggestion {
290                            path: full_path,
291                            score,
292                            item: Some(item),
293                        })
294                    }
295                })
296            })
297            .collect()
298    }
299}
300
301// ---------- name lookup with cycle detection ----------
302
303impl<'a> Resolver<'a> {
304    /// Find a child of `parent` named `target`. Used for single-segment lookups
305    /// (e.g. walking `item_summary.path` to wire up "Defined at" links).
306    pub fn find_child(
307        &mut self,
308        parent: DocRef<'a, Item>,
309        target: &str,
310    ) -> Option<DocRef<'a, Item>> {
311        self.find_named(parent, target, |_, candidate| Some(candidate))
312    }
313
314    /// Walk a sequence of name segments from `parent`.
315    pub fn find_by_path<'s>(
316        &mut self,
317        parent: DocRef<'a, Item>,
318        segments: impl IntoIterator<Item = &'s str>,
319    ) -> Option<DocRef<'a, Item>> {
320        let mut current = parent;
321        for segment in segments {
322            current = self.find_child(current, segment)?;
323        }
324        Some(current)
325    }
326
327    /// Find a child of `parent` with imported name `target`, calling `accept`
328    /// on each candidate. The first candidate for which `accept` returns
329    /// `Some` wins; `None` continues to the next candidate.
330    ///
331    /// Two phases: direct children first (real items, then non-glob Uses),
332    /// then transitive glob expansion. Methods on struct/enum/union/trait are
333    /// considered before children.
334    ///
335    /// Cycle protection: pushes `(parent, target)` onto the resolver stack;
336    /// returns `None` if the frame is already present.
337    fn find_named<F>(
338        &mut self,
339        parent: DocRef<'a, Item>,
340        target: &str,
341        mut accept: F,
342    ) -> Option<DocRef<'a, Item>>
343    where
344        F: FnMut(&mut Self, DocRef<'a, Item>) -> Option<DocRef<'a, Item>>,
345    {
346        // Indirect through a dyn-FnMut so the recursive call inside
347        // find_named_dyn doesn't re-monomorphize with a fresh `&mut F` on
348        // each level (which would blow up rustc's recursion limit).
349        self.find_named_dyn(parent, target, &mut accept)
350    }
351
352    #[allow(clippy::type_complexity, reason = "it's fine")]
353    fn find_named_dyn(
354        &mut self,
355        parent: DocRef<'a, Item>,
356        target: &str,
357        accept: &mut dyn FnMut(&mut Self, DocRef<'a, Item>) -> Option<DocRef<'a, Item>>,
358    ) -> Option<DocRef<'a, Item>> {
359        self.with_pushed(parent.into(), Some(target), |resolver| {
360            // Methods first (inherent + provided trait methods on
361            // struct/enum/union/trait). They can't be re-exports.
362            if matches!(
363                parent.inner(),
364                ItemEnum::Struct(_) | ItemEnum::Enum(_) | ItemEnum::Union(_) | ItemEnum::Trait(_)
365            ) {
366                for method in parent.methods() {
367                    if method.name() == Some(target)
368                        && let Some(found) = accept(resolver, method)
369                    {
370                        return Some(found);
371                    }
372                }
373            }
374
375            // Trait-declared associated items (required/provided methods, assoc
376            // types, assoc consts) live in `Trait.items`, not in impl blocks, so
377            // `methods()` above misses them. They're always real items, never
378            // re-exports.
379            if let ItemEnum::Trait(trait_) = parent.inner() {
380                for assoc in trait_.items.iter().filter_map(|id| parent.get(id)) {
381                    if assoc.name() == Some(target)
382                        && let Some(found) = accept(resolver, assoc.with_parent(parent))
383                    {
384                        return Some(found);
385                    }
386                }
387            }
388
389            // Phase 1: direct module/enum children, walked in source order.
390            //
391            // We collect classified candidates into a single ordered Vec so the
392            // borrow on `LazyChildren` is dropped before we re-borrow the
393            // resolver to resolve Uses. Mixing direct items and non-glob Uses
394            // by source order matters: rustdoc emission order encodes the
395            // user's intent, so e.g. a `pub use core::primitive::i128 as i128;`
396            // before a deprecated `pub mod i128` should win the lookup.
397            enum Phase1<'a> {
398                Direct(DocRef<'a, Item>),
399                NonGlob(DocRef<'a, Use>),
400            }
401            let mut phase1: Vec<Phase1<'a>> = Vec::new();
402            let mut globs = Vec::<DocRef<'a, Use>>::new();
403            for child in LazyChildren::new(parent) {
404                match child {
405                    LazyChild::Item(item) => {
406                        if item.name() == Some(target) {
407                            phase1.push(Phase1::Direct(item));
408                        }
409                    }
410                    LazyChild::NonGlob { use_item, .. } => {
411                        if use_item.item().name == target {
412                            phase1.push(Phase1::NonGlob(use_item));
413                        }
414                    }
415                    LazyChild::Glob { use_item, .. } => {
416                        globs.push(use_item);
417                    }
418                }
419            }
420
421            for cand in phase1 {
422                let resolved = match cand {
423                    Phase1::Direct(item) => item,
424                    Phase1::NonGlob(use_item) => {
425                        let imported_name: &'a str = &use_item.item().name;
426                        let Some(target_item) = resolver.follow_use(use_item, parent) else {
427                            continue;
428                        };
429                        target_item.with_name(imported_name)
430                    }
431                };
432                if let Some(found) = accept(resolver, resolved) {
433                    return Some(found);
434                }
435            }
436
437            // Phase 2: glob expansion. The resolver stack already protects
438            // against cycles via the `(parent, target)` frame, so the source
439            // module's own `find_named` recursion can't loop back here.
440            for glob_use in globs {
441                if let Some(source_module) = resolver.follow_use(glob_use, parent)
442                    && let Some(found) = resolver.find_named_dyn(source_module, target, accept)
443                {
444                    return Some(found);
445                }
446            }
447
448            None
449        })
450        .flatten()
451    }
452}
453
454// ---------- Use resolution ----------
455
456impl<'a> Resolver<'a> {
457    /// Resolve a `Use` to its target item. Tries `use_item.id` first (cheap
458    /// local lookup, can't cycle); on miss, falls back to resolving the
459    /// source path string against the parent module's path.
460    ///
461    /// `parent_module` is the module containing the `Use`, used to resolve
462    /// `self::`/`super::` prefixes in the source string.
463    pub fn follow_use(
464        &mut self,
465        use_item: DocRef<'a, Use>,
466        parent_module: DocRef<'a, Item>,
467    ) -> Option<DocRef<'a, Item>> {
468        let use_inner = use_item.item();
469        if let Some(id) = use_inner.id
470            && let Some(target) = use_item.crate_docs().get(self.navigator, &id)
471        {
472            return Some(target);
473        }
474
475        self.with_pushed(use_item.into(), None, |resolver| {
476            let module_path = parent_module.containing_module_path();
477            let rewritten = rewrite_relative_prefix(&module_path, &use_inner.source)?;
478            resolver.resolve_path(&rewritten, &mut vec![])
479        })
480        .flatten()
481    }
482
483    /// Resolve a [`LazyChild`] to a concrete item. For non-glob Uses, the
484    /// imported name is preserved on the returned `DocRef`.
485    pub fn resolve_lazy_child(&mut self, child: LazyChild<'a>) -> Option<DocRef<'a, Item>> {
486        match child {
487            LazyChild::Item(item) => Some(item),
488            LazyChild::NonGlob { use_item, parent } => {
489                let name: &'a str = &use_item.item().name;
490                self.follow_use(use_item, parent).map(|i| i.with_name(name))
491            }
492            LazyChild::Glob { use_item, parent } => self.follow_use(use_item, parent),
493        }
494    }
495}
496
497// ---------- child iteration ----------
498
499impl<'a> Resolver<'a> {
500    /// Children of `item` with `Use`s resolved and globs expanded. Equivalent
501    /// to the old `item.child_items()`.
502    pub fn children(&mut self, item: DocRef<'a, Item>) -> Vec<DocRef<'a, Item>> {
503        let mut out = Vec::new();
504        self.collect_children(item, false, ParentRef::from(item), &mut out);
505        out
506    }
507
508    /// Children of `item`, but yield `Use` items themselves rather than their
509    /// targets. Used by the search indexer to record `use` statements.
510    pub fn children_including_uses(&mut self, item: DocRef<'a, Item>) -> Vec<DocRef<'a, Item>> {
511        let mut out = Vec::new();
512        self.collect_children(item, true, ParentRef::from(item), &mut out);
513        out
514    }
515
516    /// Resolve a slice of `Id`s in `parent`, expanding `Use` items. Replaces
517    /// the old `id_iter`.
518    pub fn ids(&mut self, parent: DocRef<'a, Item>, ids: &'a [Id]) -> Vec<DocRef<'a, Item>> {
519        let mut out = Vec::new();
520        self.collect_ids(parent, ids, false, Some(ParentRef::from(parent)), &mut out);
521        out
522    }
523
524    /// Like [`Self::ids`] but yields `Use` items themselves instead of their
525    /// targets.
526    pub fn ids_including_uses(
527        &mut self,
528        parent: DocRef<'a, Item>,
529        ids: &'a [Id],
530    ) -> Vec<DocRef<'a, Item>> {
531        let mut out = Vec::new();
532        self.collect_ids(parent, ids, true, Some(ParentRef::from(parent)), &mut out);
533        out
534    }
535
536    fn collect_children(
537        &mut self,
538        item: DocRef<'a, Item>,
539        include_uses: bool,
540        parent_ref: ParentRef<'a>,
541        out: &mut Vec<DocRef<'a, Item>>,
542    ) {
543        match item.inner() {
544            ItemEnum::Module(module) => {
545                self.collect_ids(item, &module.items, include_uses, Some(parent_ref), out);
546            }
547            ItemEnum::Enum(enum_item) => {
548                self.collect_ids(
549                    item,
550                    &enum_item.variants,
551                    include_uses,
552                    Some(parent_ref),
553                    out,
554                );
555                out.extend(item.methods());
556            }
557            ItemEnum::Struct(_) | ItemEnum::Union(_) => {
558                out.extend(item.methods());
559            }
560            ItemEnum::Use(use_item) => {
561                let use_ref = item.build_ref(use_item);
562                self.collect_use_children(use_ref, item, include_uses, out);
563            }
564            _ => {}
565        }
566    }
567
568    fn collect_ids(
569        &mut self,
570        parent: DocRef<'a, Item>,
571        ids: &'a [Id],
572        include_uses: bool,
573        parent_ref: Option<ParentRef<'a>>,
574        out: &mut Vec<DocRef<'a, Item>>,
575    ) {
576        for id in ids {
577            let Some(item) = parent.get(id) else { continue };
578            if let ItemEnum::Use(use_item) = item.inner() {
579                if include_uses {
580                    out.push(item);
581                    continue;
582                }
583                let use_ref = item.build_ref(use_item);
584                self.collect_use_children(use_ref, parent, include_uses, out);
585            } else {
586                let item = match parent_ref {
587                    Some(p) => item.with_parent(p),
588                    None => item,
589                };
590                out.push(item);
591            }
592        }
593    }
594
595    fn collect_use_children(
596        &mut self,
597        use_ref: DocRef<'a, Use>,
598        parent: DocRef<'a, Item>,
599        include_uses: bool,
600        out: &mut Vec<DocRef<'a, Item>>,
601    ) {
602        let Some(source_item) = self.follow_use(use_ref, parent) else {
603            return;
604        };
605        let use_item = use_ref.item();
606        if use_item.is_glob {
607            match source_item.inner() {
608                ItemEnum::Module(module) => {
609                    self.collect_ids(source_item, &module.items, include_uses, None, out);
610                }
611                ItemEnum::Enum(enum_item) => {
612                    self.collect_ids(source_item, &enum_item.variants, include_uses, None, out);
613                }
614                _ => {}
615            }
616        } else {
617            // Non-glob Use: yield the source item with the imported name. We
618            // intentionally don't chain through nested `pub use`s here — if
619            // the source is itself a Use, that's what the caller sees. The
620            // user can follow the link to walk further.
621            out.push(source_item.with_name(&use_item.name));
622        }
623    }
624}
625
626// ---------- id-path resolution ----------
627
628impl<'a> Resolver<'a> {
629    /// Resolve an `Id` from `origin`'s crate to a `DocRef`. The id is looked
630    /// up in `origin`'s rustdoc paths map; for external-crate ids this
631    /// crosses the crate boundary, loading the target crate if needed and
632    /// walking its public path.
633    pub fn get_path(&mut self, origin: DocRef<'a, Item>, id: Id) -> Option<DocRef<'a, Item>> {
634        let item_summary = origin.crate_docs().paths.get(&id)?;
635        let crate_ = origin
636            .crate_docs()
637            .traverse_to_crate_by_id(self.navigator, item_summary.crate_id)?;
638        let root = crate_.root_item(self.navigator);
639        self.find_by_path(root, item_summary.path.iter().skip(1).map(String::as_str))
640    }
641
642    /// Get an item from a sequence of `Id`s starting at the named crate's
643    /// root. Returns the resolved item plus the path of segment names.
644    pub fn get_item_from_id_path(
645        &mut self,
646        crate_name: &str,
647        ids: &[u32],
648    ) -> Option<(DocRef<'a, Item>, Vec<&'a str>)> {
649        let mut path = vec![];
650        let crate_docs = self.navigator.load_crate(crate_name, &VersionReq::STAR)?;
651        let mut item = crate_docs.get(self.navigator, &crate_docs.root)?;
652        path.push(item.crate_docs().name());
653        for id in ids {
654            item = item.get(&Id(*id))?;
655            if let ItemEnum::Use(use_item) = item.inner() {
656                let use_ref = item.build_ref(use_item);
657                let parent = item;
658                item = self
659                    .follow_use(use_ref, parent)
660                    .or_else(|| self.resolve_path(&use_item.source, &mut vec![]))?;
661                if !use_item.is_glob {
662                    item.set_name(&use_item.name);
663                }
664            } else if let Some(name) = item.name() {
665                path.push(name);
666            }
667        }
668        Some((item, path))
669    }
670}
671
672// ---------- helpers ----------
673
674/// Parse a path segment that may carry a rustdoc kind discriminator prefix,
675/// e.g. `"fn@foo"` or `"struct@Vec"`.
676fn parse_discriminated_segment(segment: &str) -> (Option<ItemKind>, &str) {
677    let Some(at) = segment.find('@') else {
678        return (None, segment);
679    };
680    let (disc, name) = (&segment[..at], &segment[at + 1..]);
681    match disc {
682        "mod" | "module" => (Some(ItemKind::Module), name),
683        "struct" => (Some(ItemKind::Struct), name),
684        "enum" => (Some(ItemKind::Enum), name),
685        "union" => (Some(ItemKind::Union), name),
686        "trait" => (Some(ItemKind::Trait), name),
687        "traitalias" => (Some(ItemKind::TraitAlias), name),
688        "fn" | "function" | "method" => (Some(ItemKind::Function), name),
689        "tyalias" | "typealias" => (Some(ItemKind::TypeAlias), name),
690        "type" => (Some(ItemKind::AssocType), name),
691        "const" | "constant" => (Some(ItemKind::Constant), name),
692        "static" => (Some(ItemKind::Static), name),
693        "macro" => (Some(ItemKind::Macro), name),
694        "attr" => (Some(ItemKind::ProcAttribute), name),
695        "derive" => (Some(ItemKind::ProcDerive), name),
696        "prim" | "primitive" => (Some(ItemKind::Primitive), name),
697        "field" => (Some(ItemKind::StructField), name),
698        "variant" => (Some(ItemKind::Variant), name),
699        "value" => (None, name),
700        _ => (None, segment),
701    }
702}
703
704/// Rewrite a relative path prefix (`crate::`, `self::`, `super::…`) against
705/// `module_path`, returning the absolute equivalent. Non-prefixed paths are
706/// returned unchanged. Returns `None` only when `super::` walks above the
707/// crate root.
708fn rewrite_relative_prefix(module_path: &[&str], source: &str) -> Option<String> {
709    let mut supers = 0;
710    let mut rest = source;
711    while let Some(tail) = rest.strip_prefix("super::") {
712        supers += 1;
713        rest = tail;
714    }
715    if supers > 0 || rest == "super" {
716        let supers = if rest == "super" { supers + 1 } else { supers };
717        let tail = if rest == "super" { "" } else { rest };
718        let remaining = module_path.len().checked_sub(supers)?;
719        if remaining == 0 {
720            return None;
721        }
722        let prefix = module_path[..remaining].join("::");
723        return Some(if tail.is_empty() {
724            prefix
725        } else {
726            format!("{prefix}::{tail}")
727        });
728    }
729
730    if let Some(tail) = source.strip_prefix("self::") {
731        return Some(format!("{}::{}", module_path.join("::"), tail));
732    }
733    if source == "self" {
734        return Some(module_path.join("::"));
735    }
736
737    if let Some(tail) = source.strip_prefix("crate::") {
738        let crate_name = module_path.first().copied()?;
739        return Some(format!("{crate_name}::{tail}"));
740    }
741    if source == "crate" {
742        return module_path.first().copied().map(String::from);
743    }
744
745    Some(source.to_owned())
746}