Skip to main content

cargo_rdme/transform/intralinks/
rustdoc.rs

1use crate::PackageTarget;
2use crate::transform::intralinks::ItemPath;
3use crate::transform::intralinks::links::Link;
4use crate::transform::{IntralinkError, IntralinksConfig, IntralinksDocsConfig};
5use itertools::Itertools;
6use rustdoc_json::BuildError;
7use rustdoc_types::{
8    Crate, ExternalCrate, Id as ItemId, Impl, Item, ItemEnum, ItemSummary, MacroKind, Primitive,
9    Struct, StructKind, Trait, Type,
10};
11use rustdoc_types::{Enum, ProcMacro, Union};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15// TODO Remove this when rustdoc json stabilizes (https://github.com/rust-lang/rust/issues/76578).
16pub const EXPECTED_RUST_TOOLCHAIN: &str = "nightly-2026-08-05";
17const EXPECTED_RUSTDOC_FORMAT_VERSION: u32 = 61;
18
19pub fn is_expected_rust_toolchain_installed() -> Result<bool, IntralinkError> {
20    rustup_toolchain::is_installed(EXPECTED_RUST_TOOLCHAIN)
21        .map_err(|error| IntralinkError::RustupToolchain { error })
22}
23
24pub fn install_expected_rust_toolchain() -> Result<(), IntralinkError> {
25    rustup_toolchain::install(EXPECTED_RUST_TOOLCHAIN)
26        .map_err(|error| IntralinkError::RustupToolchain { error })
27}
28
29fn crate_from_file(path: &Path) -> Result<Crate, IntralinkError> {
30    let json = std::fs::read_to_string(path)
31        .map_err(|io_error| IntralinkError::ReadRustdocError { io_error })?;
32    serde_json::from_str(&json)
33        .map_err(|serde_error| IntralinkError::ParseRustdocError { serde_error })
34}
35
36fn crate_rustdoc_intralinks(c: &Crate) -> &HashMap<String, ItemId> {
37    &c.index.get(&c.root).expect("root id not present in index").links
38}
39
40#[derive(Debug, Clone)]
41struct ItemInfo<'a> {
42    crate_id: u32,
43    path: ItemPath<'a>,
44    kind: ItemKind,
45    parent_kind: Option<ItemKind>,
46}
47
48impl<'a> ItemInfo<'a> {
49    fn new(
50        crate_id: u32,
51        path: ItemPath<'a>,
52        kind: ItemKind,
53        parent_kind: Option<ItemKind>,
54    ) -> ItemInfo<'a> {
55        ItemInfo { crate_id, path, kind, parent_kind }
56    }
57
58    fn from(
59        item_summary: &'a ItemSummary,
60        parent_kind: Option<ItemKind>,
61        item_context: ItemContext,
62    ) -> ItemInfo<'a> {
63        ItemInfo::new(
64            item_summary.crate_id,
65            ItemPath::new(&item_summary.path),
66            ItemKind::from_rustdoc_item_kind(item_summary.kind, item_context),
67            parent_kind,
68        )
69    }
70
71    /// Merges all the information of both items.
72    fn merge(&self, other: &ItemInfo<'a>) -> Option<ItemInfo<'a>> {
73        if self.crate_id != other.crate_id {
74            return None;
75        }
76        if self.path != other.path {
77            return None;
78        }
79        if self.kind != other.kind {
80            return None;
81        }
82
83        if self.parent_kind.zip(other.parent_kind).is_some_and(|(s, o)| s != o) {
84            return None;
85        }
86
87        let merged = ItemInfo {
88            crate_id: self.crate_id,
89            path: self.path.clone(),
90            kind: self.kind,
91            parent_kind: self.parent_kind.or(other.parent_kind),
92        };
93
94        Some(merged)
95    }
96}
97
98#[derive(PartialEq, Eq, Clone, Copy, Debug)]
99pub enum ItemKind {
100    Module,
101    ExternCrate,
102    Use,
103    Struct,
104    StructField,
105    Union,
106    Enum,
107    Variant,
108    Function,
109    TypeAlias,
110    Constant,
111    Trait,
112    TraitAlias,
113    Impl,
114    Static,
115    ExternType,
116    Macro,
117    ProcAttribute,
118    ProcDerive,
119    AssocConst,
120    AssocType,
121    Primitive,
122    Keyword,
123    Attribute,
124
125    // Kinds that do not exist in rustdoc_types::ItemKind:
126    Method,
127    TyMethod,
128}
129
130impl ItemKind {
131    fn from_rustdoc_item_kind(
132        kind: rustdoc_types::ItemKind,
133        item_context: ItemContext,
134    ) -> ItemKind {
135        match kind {
136            rustdoc_types::ItemKind::Module => ItemKind::Module,
137            rustdoc_types::ItemKind::ExternCrate => ItemKind::ExternCrate,
138            rustdoc_types::ItemKind::Use => ItemKind::Use,
139            rustdoc_types::ItemKind::Struct => ItemKind::Struct,
140            rustdoc_types::ItemKind::StructField => ItemKind::StructField,
141            rustdoc_types::ItemKind::Union => ItemKind::Union,
142            rustdoc_types::ItemKind::Enum => ItemKind::Enum,
143            rustdoc_types::ItemKind::Variant => ItemKind::Variant,
144            rustdoc_types::ItemKind::Function => match item_context {
145                ItemContext::Normal => ItemKind::Function,
146                ItemContext::Impl => ItemKind::Method,
147                ItemContext::Trait => ItemKind::TyMethod,
148            },
149            rustdoc_types::ItemKind::TypeAlias => ItemKind::TypeAlias,
150            rustdoc_types::ItemKind::Constant => ItemKind::Constant,
151            rustdoc_types::ItemKind::Trait => ItemKind::Trait,
152            rustdoc_types::ItemKind::TraitAlias => ItemKind::TraitAlias,
153            rustdoc_types::ItemKind::Impl => ItemKind::Impl,
154            rustdoc_types::ItemKind::Static => ItemKind::Static,
155            rustdoc_types::ItemKind::ExternType => ItemKind::ExternType,
156            rustdoc_types::ItemKind::Macro => ItemKind::Macro,
157            rustdoc_types::ItemKind::ProcAttribute => ItemKind::ProcAttribute,
158            rustdoc_types::ItemKind::ProcDerive => ItemKind::ProcDerive,
159            rustdoc_types::ItemKind::AssocConst => ItemKind::AssocConst,
160            rustdoc_types::ItemKind::AssocType => ItemKind::AssocType,
161            rustdoc_types::ItemKind::Primitive => ItemKind::Primitive,
162            rustdoc_types::ItemKind::Keyword => ItemKind::Keyword,
163            rustdoc_types::ItemKind::Attribute => ItemKind::Attribute,
164        }
165    }
166
167    fn of_item(item: &Item, item_context: ItemContext) -> ItemKind {
168        match item.inner {
169            ItemEnum::Module(_) => ItemKind::Module,
170            ItemEnum::ExternCrate { .. } => ItemKind::ExternCrate,
171            ItemEnum::Use(_) => ItemKind::Use,
172            ItemEnum::Union(_) => ItemKind::Union,
173            ItemEnum::Struct(_) => ItemKind::Struct,
174            ItemEnum::StructField(_) => ItemKind::StructField,
175            ItemEnum::Enum(_) => ItemKind::Enum,
176            ItemEnum::Variant(_) => ItemKind::Variant,
177            ItemEnum::Function(_) => match item_context {
178                ItemContext::Normal => ItemKind::Function,
179                ItemContext::Impl => ItemKind::Method,
180                ItemContext::Trait => ItemKind::TyMethod,
181            },
182            ItemEnum::Trait(_) => ItemKind::Trait,
183            ItemEnum::TraitAlias(_) => ItemKind::TraitAlias,
184            ItemEnum::Impl(_) => ItemKind::Impl,
185            ItemEnum::TypeAlias(_) => ItemKind::TypeAlias,
186            ItemEnum::Constant { .. } => ItemKind::Constant,
187            ItemEnum::Static(_) => ItemKind::Static,
188            ItemEnum::ExternType => ItemKind::ExternType,
189            ItemEnum::Macro(_) => ItemKind::Macro,
190            ItemEnum::ProcMacro(ProcMacro { kind: MacroKind::Bang, .. }) => ItemKind::Macro,
191            ItemEnum::ProcMacro(ProcMacro { kind: MacroKind::Derive, .. }) => ItemKind::ProcDerive,
192            ItemEnum::ProcMacro(ProcMacro { kind: MacroKind::Attr, .. }) => ItemKind::ProcAttribute,
193            ItemEnum::Primitive(_) => ItemKind::Primitive,
194            ItemEnum::AssocConst { .. } => ItemKind::AssocConst,
195            ItemEnum::AssocType { .. } => ItemKind::AssocType,
196        }
197    }
198}
199
200fn child_item_ids<'a>(item: &'a Item) -> Box<dyn Iterator<Item = ItemId> + 'a> {
201    match &item.inner {
202        ItemEnum::Struct(Struct { kind, impls, .. }) => {
203            let fields_ids: Box<dyn Iterator<Item = ItemId>> = match kind {
204                StructKind::Unit => Box::new(std::iter::empty()),
205                StructKind::Tuple(ids) => Box::new(ids.iter().copied().flatten()),
206                StructKind::Plain { fields, .. } => Box::new(fields.iter().copied()),
207            };
208
209            Box::new(fields_ids.chain(impls.iter().copied()))
210        }
211        ItemEnum::Impl(Impl { trait_: Some(_), .. }) => Box::new(std::iter::empty()),
212        ItemEnum::Impl(Impl { items: item_ids, for_, .. }) => match for_ {
213            Type::ResolvedPath(_) => Box::new(item_ids.iter().copied()),
214            _ => Box::new(std::iter::empty()),
215        },
216        ItemEnum::Union(Union { fields, impls, .. }) => {
217            Box::new(fields.iter().chain(impls.iter()).copied())
218        }
219        ItemEnum::Enum(Enum { variants, impls, .. }) => {
220            Box::new(variants.iter().chain(impls.iter()).copied())
221        }
222        ItemEnum::Primitive(Primitive { impls, .. }) => Box::new(impls.iter().copied()),
223        ItemEnum::Trait(Trait { items, .. }) => {
224            // We ignore the implementations of the trait as their items are not part of the trait
225            // itself.
226            Box::new(items.iter().copied())
227        }
228
229        ItemEnum::Function(_)
230        | ItemEnum::ExternCrate { .. }
231        | ItemEnum::Use(_)
232        | ItemEnum::Module(_)
233        | ItemEnum::Constant { .. }
234        | ItemEnum::Static(_)
235        | ItemEnum::Macro(_)
236        | ItemEnum::ProcMacro(_)
237        | ItemEnum::AssocConst { .. }
238        | ItemEnum::AssocType { .. }
239        | ItemEnum::StructField(_)
240        | ItemEnum::Variant(_)
241        | ItemEnum::ExternType
242        | ItemEnum::TraitAlias(_)
243        | ItemEnum::TypeAlias(_) => Box::new(std::iter::empty()),
244    }
245}
246
247#[derive(Clone, Copy, Debug)]
248enum ItemContext {
249    Normal,
250    Impl,
251    Trait,
252}
253
254fn get_item_info<'a>(
255    item_id: ItemId,
256    parent_path: &ItemPath<'a>,
257    parent_kind: Option<ItemKind>,
258    item_context: ItemContext,
259    rustdoc_crate: &'a Crate,
260) -> Option<ItemInfo<'a>> {
261    match rustdoc_crate.paths.get(&item_id) {
262        Some(item_summary) => Some(ItemInfo::from(item_summary, parent_kind, item_context)),
263        None => rustdoc_crate.index.get(&item_id).map(|item| {
264            let path = match item.name.as_ref() {
265                None => parent_path.clone(),
266                Some(name) => parent_path.add(name.clone()),
267            };
268            let item_kind = ItemKind::of_item(item, item_context);
269
270            ItemInfo::new(item.crate_id, path, item_kind, parent_kind)
271        }),
272    }
273}
274
275fn transitive_items<'a>(
276    item_id: ItemId,
277    item_info: &ItemInfo<'a>,
278    item_context: ItemContext,
279    rustdoc_crate: &'a Crate,
280    items_info: &mut HashMap<ItemId, ItemInfo<'a>>,
281) {
282    if item_info.kind != ItemKind::Impl {
283        items_info
284            .entry(item_id)
285            .and_modify(|existing_item_info| {
286                *existing_item_info = existing_item_info.merge(item_info).unwrap_or_else(|| {
287                    panic!("unmergeable item info: {item_info:?} and {existing_item_info:?}")
288                });
289            })
290            .or_insert_with(|| item_info.clone());
291    }
292
293    let Some(item) = rustdoc_crate.index.get(&item_id) else {
294        // This item is not in the index for some reason...
295        return;
296    };
297
298    let inner_item_context = match item.inner {
299        ItemEnum::Trait(_) => ItemContext::Trait,
300        ItemEnum::Impl(_) => ItemContext::Impl,
301        _ => item_context,
302    };
303
304    for inner_item_id in child_item_ids(item) {
305        // The inner_item_parent_kind is not just `item_info.kind` because we need to skip
306        // kinds like `impl` blocks.
307        let inner_item_parent_kind = match item.name {
308            Some(_) => Some(item_info.kind),
309            None => item_info.parent_kind,
310        };
311
312        let inner_item_info = get_item_info(
313            inner_item_id,
314            &item_info.path,
315            inner_item_parent_kind,
316            inner_item_context,
317            rustdoc_crate,
318        );
319
320        if let Some(inner_item_info) = inner_item_info {
321            transitive_items(
322                inner_item_id,
323                &inner_item_info,
324                inner_item_context,
325                rustdoc_crate,
326                items_info,
327            );
328        }
329    }
330}
331
332pub struct IntralinkResolver<'a> {
333    link_url: HashMap<Link, String>,
334    config: &'a IntralinksDocsConfig,
335    package_name: &'a str,
336}
337
338impl<'a> IntralinkResolver<'a> {
339    pub fn new(package_name: &'a str, config: &'a IntralinksDocsConfig) -> IntralinkResolver<'a> {
340        IntralinkResolver { link_url: HashMap::new(), package_name, config }
341    }
342
343    fn url_segment(kind: ItemKind, name: &str) -> String {
344        match kind {
345            ItemKind::Module => format!("{name}/"),
346            ItemKind::Struct => format!("struct.{name}.html"),
347            ItemKind::StructField => format!("#structfield.{name}"),
348            ItemKind::Union => format!("union.{name}.html"),
349            ItemKind::Enum => format!("enum.{name}.html"),
350            ItemKind::Variant => format!("#variant.{name}"),
351            ItemKind::Function => format!("fn.{name}.html"),
352            ItemKind::Method => format!("#method.{name}"),
353            ItemKind::TyMethod => format!("#tymethod.{name}"),
354            ItemKind::TypeAlias => format!("type.{name}.html"),
355            ItemKind::Constant => format!("const.{name}.html"),
356            ItemKind::Trait => format!("trait.{name}.html"),
357            ItemKind::TraitAlias => format!("traitalias.{name}.html"),
358            ItemKind::Static => format!("static.{name}.html"),
359            ItemKind::Macro => format!("macro.{name}.html"),
360            ItemKind::ProcAttribute => format!("attr.{name}.html"),
361            ItemKind::ProcDerive => format!("derive.{name}.html"),
362            ItemKind::AssocConst => {
363                format!("#associatedconstant.{name}")
364            }
365            ItemKind::AssocType => format!("#associatedtype.{name}"),
366            ItemKind::Primitive => format!("primitive.{name}.html"),
367
368            ItemKind::Keyword
369            | ItemKind::ExternCrate
370            | ItemKind::Use
371            | ItemKind::Impl
372            | ItemKind::ExternType
373            | ItemKind::Attribute => {
374                unreachable!("items of kind {:?} cannot be intralinked to", kind);
375            }
376        }
377    }
378
379    fn is_stdlib_crate(external_crate: &ExternalCrate) -> bool {
380        external_crate
381            .html_root_url
382            .as_deref()
383            .is_some_and(|base_url| base_url.starts_with("https://doc.rust-lang.org/"))
384    }
385
386    fn make_docs_rs_url(
387        base_url: &str,
388        package_name: &str,
389        version: &str,
390        url_path: &str,
391    ) -> String {
392        format!("{base_url}/{package_name}/{version}/{url_path}")
393    }
394
395    fn make_flat_url(base_url: &str, url_path: &str) -> String {
396        format!("{base_url}/{url_path}")
397    }
398
399    fn add(
400        &mut self,
401        link: Link,
402        item_info: &ItemInfo,
403        external_crates: &HashMap<u32, ExternalCrate>,
404    ) {
405        let path_segment_kind = |i: usize| match item_info.path.len() - i {
406            1 => item_info.kind,
407            2 => item_info.parent_kind.unwrap_or(ItemKind::Module),
408            _ => ItemKind::Module,
409        };
410        let url_path = item_info
411            .path
412            .segments()
413            .enumerate()
414            .map(|(i, segment)| (segment, path_segment_kind(i)))
415            .map(|(segment, item_kind)| IntralinkResolver::url_segment(item_kind, segment))
416            .join("");
417
418        let url = match item_info.crate_id {
419            // Local crate has id 0.
420            0 => match self.config {
421                IntralinksDocsConfig::DocsRs { base_url, version } => {
422                    let base_url = base_url.as_deref().unwrap_or("https://docs.rs");
423                    let version = version.as_deref().unwrap_or("latest");
424                    let package_name = &self.package_name;
425
426                    Self::make_docs_rs_url(base_url, package_name, version, &url_path)
427                }
428                IntralinksDocsConfig::Flat { base_url } => Self::make_flat_url(base_url, &url_path),
429            },
430            // External crate
431            _ => {
432                let Some(external_crate) = external_crates.get(&item_info.crate_id) else {
433                    return;
434                };
435
436                match external_crate.html_root_url.as_deref() {
437                    Some(base_url) => {
438                        let base_url = match Self::is_stdlib_crate(external_crate) {
439                            true => {
440                                // TODO Once we are able to use the stable version we can remove this
441                                //      (https://github.com/rust-lang/rust/issues/76578).
442                                base_url
443                                    .strip_suffix("/nightly/")
444                                    .map_or_else(|| base_url.to_owned(), |p| format!("{p}/stable/"))
445                            }
446                            false => base_url.to_owned(),
447                        };
448
449                        format!("{base_url}{url_path}")
450                    }
451                    None => match self.config {
452                        IntralinksDocsConfig::DocsRs { base_url, .. } => {
453                            let base_url = base_url.as_deref().unwrap_or("https://docs.rs");
454                            let crate_name = &external_crate.name;
455
456                            // TODO We are using the crate name instead of the package name: that means that
457                            //      we might generate a wrong url. In most cases the crate name matches the
458                            //      package name. When it doesn't it is often because underscores in the
459                            //      crate name becomes dashes in the package name. Fortunately `docs.rs`
460                            //      will redirect in that case (e.g. https://docs.rs/tower_service/ will
461                            //      redirect to https://docs.rs/tower-service/latest/tower_service/).
462                            // TODO We shouldn't hardcode "latest" here: we should get that information from
463                            //      the version rustdoc determined the crate was using.
464                            Self::make_docs_rs_url(base_url, crate_name, "latest", &url_path)
465                        }
466                        IntralinksDocsConfig::Flat { base_url } => {
467                            Self::make_flat_url(base_url, &url_path)
468                        }
469                    },
470                }
471            }
472        };
473
474        self.link_url.insert(link, url);
475    }
476
477    pub fn resolve_link(&self, link: &Link) -> Option<&str> {
478        self.link_url.get(link).map(String::as_str)
479    }
480
481    pub fn is_intralink(link: &Link) -> bool {
482        let has_lone_colon = || link.raw_link.replace("::", "").contains(':');
483
484        !link.symbol().is_empty() && !link.raw_link.contains('/') && !has_lone_colon()
485    }
486}
487
488fn run_rustdoc(
489    package_target: &PackageTarget,
490    workspace_package: Option<&str>,
491    manifest_path: &PathBuf,
492    config: &IntralinksConfig,
493) -> Result<Crate, IntralinkError> {
494    let rustdoc_json_path: PathBuf = {
495        let target: rustdoc_json::PackageTarget = match package_target {
496            PackageTarget::Bin { crate_name } => {
497                rustdoc_json::PackageTarget::Bin(crate_name.clone())
498            }
499            PackageTarget::Lib => rustdoc_json::PackageTarget::Lib,
500        };
501        let mut stderr = Vec::new();
502
503        let toolchain = if let Some(toolchain) = &config.rustdoc_toolchain {
504            if toolchain == "default" { None } else { Some(toolchain.as_str()) }
505        } else {
506            match is_expected_rust_toolchain_installed()? {
507                true => Some(EXPECTED_RUST_TOOLCHAIN),
508                false => {
509                    return Err(IntralinkError::RustToolchainNotInstalled {
510                        expected: EXPECTED_RUST_TOOLCHAIN,
511                    });
512                }
513            }
514        };
515
516        let mut builder = rustdoc_json::Builder::default();
517        if let Some(toolchain) = toolchain {
518            builder = builder.toolchain(toolchain);
519        }
520        builder = builder
521            .manifest_path(manifest_path)
522            .document_private_items(true)
523            .all_features(config.all_features.unwrap_or_default())
524            .features(config.features.clone().unwrap_or_default())
525            .no_default_features(config.no_default_features.unwrap_or_default())
526            .quiet(true)
527            .color(rustdoc_json::Color::Never)
528            .package_target(target);
529
530        if let Some(package) = workspace_package {
531            builder = builder.package(package);
532        }
533
534        let result = builder.build_with_captured_output(std::io::sink(), &mut stderr);
535
536        result.map_err(|error| match error {
537            BuildError::BuildRustdocJsonError => match stderr.is_empty() {
538                true => IntralinkError::BuildRustdocError {
539                    stderr: "Weirdly, rustdoc did not write anything to stderr".to_owned(),
540                },
541                false => IntralinkError::BuildRustdocError {
542                    stderr: String::from_utf8_lossy(&stderr).into_owned(),
543                },
544            },
545            e => IntralinkError::RustdocError { error: e },
546        })?
547    };
548
549    let rustdoc_crate = crate_from_file(&rustdoc_json_path)?;
550
551    match rustdoc_crate.format_version {
552        EXPECTED_RUSTDOC_FORMAT_VERSION => Ok(rustdoc_crate),
553        format_version => Err(IntralinkError::UnsupportedRustdocFormatVersion {
554            version: format_version,
555            expected_version: EXPECTED_RUSTDOC_FORMAT_VERSION,
556        }),
557    }
558}
559
560/// Rustdoc's `paths` map classifies inherent and trait methods as `Function`, and it doesn't
561/// carry parent information. We recover both by looking at the parent path segment: if the
562/// parent is a type-like item, the entry is an associated item under an impl (or trait) block.
563fn infer_context_from_path(
564    path: &ItemPath<'_>,
565    path_to_kind: &HashMap<ItemPath<'_>, rustdoc_types::ItemKind>,
566) -> (ItemContext, Option<ItemKind>) {
567    let Some(parent_path) = path.parent() else {
568        return (ItemContext::Normal, None);
569    };
570
571    match path_to_kind.get(&parent_path) {
572        Some(rustdoc_types::ItemKind::Struct) => (ItemContext::Impl, Some(ItemKind::Struct)),
573        Some(rustdoc_types::ItemKind::Enum) => (ItemContext::Impl, Some(ItemKind::Enum)),
574        Some(rustdoc_types::ItemKind::Union) => (ItemContext::Impl, Some(ItemKind::Union)),
575        Some(rustdoc_types::ItemKind::Primitive) => (ItemContext::Impl, Some(ItemKind::Primitive)),
576        Some(rustdoc_types::ItemKind::TypeAlias) => (ItemContext::Impl, Some(ItemKind::TypeAlias)),
577        Some(rustdoc_types::ItemKind::Trait) => (ItemContext::Trait, Some(ItemKind::Trait)),
578        Some(rustdoc_types::ItemKind::Module) => (ItemContext::Normal, Some(ItemKind::Module)),
579        _ => (ItemContext::Normal, None),
580    }
581}
582
583fn items_info(rustdoc_crate: &Crate) -> HashMap<ItemId, ItemInfo<'_>> {
584    let mut items_info: HashMap<ItemId, ItemInfo<'_>> =
585        HashMap::with_capacity(rustdoc_crate.index.len());
586
587    let path_to_kind: HashMap<ItemPath<'_>, rustdoc_types::ItemKind> =
588        rustdoc_crate.paths.values().map(|s| (ItemPath::new(&s.path), s.kind)).collect();
589
590    for (&item_id, item_summary) in &rustdoc_crate.paths {
591        let item_path = ItemPath::new(&item_summary.path);
592        let (item_context, parent_kind) = infer_context_from_path(&item_path, &path_to_kind);
593        let item_info = ItemInfo::from(item_summary, parent_kind, item_context);
594
595        transitive_items(item_id, &item_info, item_context, rustdoc_crate, &mut items_info);
596    }
597
598    items_info
599}
600
601pub fn create_intralink_resolver<'a>(
602    package_name: &'a str,
603    package_target: &PackageTarget,
604    workspace_package: Option<&str>,
605    manifest_path: &PathBuf,
606    config: &'a IntralinksConfig,
607) -> Result<IntralinkResolver<'a>, IntralinkError> {
608    let rustdoc_crate = run_rustdoc(package_target, workspace_package, manifest_path, config)?;
609
610    let items_info: HashMap<ItemId, ItemInfo<'_>> = items_info(&rustdoc_crate);
611    let links_items_id = crate_rustdoc_intralinks(&rustdoc_crate);
612    let mut intralink_resolver = IntralinkResolver::new(package_name, &config.docs);
613
614    for (link, item_id) in links_items_id {
615        let link = Link::new(link.clone());
616        let Some(item_info) = items_info.get(item_id) else {
617            // We will fail when we try to create the link and will emit a warning there.
618            continue;
619        };
620
621        intralink_resolver.add(link, item_info, &rustdoc_crate.external_crates);
622    }
623
624    Ok(intralink_resolver)
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use pretty_assertions::assert_eq;
631
632    #[test]
633    fn test_rustdoc_format_supported_version() {
634        assert_eq!(rustdoc_types::FORMAT_VERSION, EXPECTED_RUSTDOC_FORMAT_VERSION);
635    }
636
637    fn make_item_info(
638        crate_id: u32,
639        path: &'static [&'static str],
640        kind: ItemKind,
641        parent_kind: Option<ItemKind>,
642    ) -> ItemInfo<'static> {
643        let segments: &'static [String] = Box::leak(
644            path.iter().map(|&s| s.to_owned()).collect::<Vec<String>>().into_boxed_slice(),
645        );
646
647        ItemInfo::new(crate_id, ItemPath::new(segments), kind, parent_kind)
648    }
649
650    #[test]
651    fn test_item_info_merge_identical() {
652        let a = make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
653        let b = a.clone();
654
655        let merged = a.merge(&b).expect("identical items should merge");
656
657        assert_eq!(merged.crate_id, 0);
658        assert_eq!(merged.kind, ItemKind::Struct);
659        assert_eq!(merged.parent_kind, Some(ItemKind::Module));
660    }
661
662    #[test]
663    fn test_item_info_merge_fills_missing_parent_kind() {
664        let with_parent =
665            make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
666        let without_parent = make_item_info(0, &["foo", "Bar"], ItemKind::Struct, None);
667
668        let merged_a = with_parent.merge(&without_parent).expect("compatible parent_kinds");
669        let merged_b = without_parent.merge(&with_parent).expect("compatible parent_kinds");
670
671        assert_eq!(merged_a.parent_kind, Some(ItemKind::Module));
672        assert_eq!(merged_b.parent_kind, Some(ItemKind::Module));
673    }
674
675    #[test]
676    fn test_item_info_merge_rejects_mismatch() {
677        let base = make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
678
679        let different_crate =
680            make_item_info(1, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
681        let different_path =
682            make_item_info(0, &["other", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
683        let different_kind =
684            make_item_info(0, &["foo", "Bar"], ItemKind::Enum, Some(ItemKind::Module));
685        let different_parent =
686            make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Struct));
687
688        assert!(base.merge(&different_crate).is_none());
689        assert!(base.merge(&different_path).is_none());
690        assert!(base.merge(&different_kind).is_none());
691        assert!(base.merge(&different_parent).is_none());
692    }
693
694    #[test]
695    fn test_is_intralink_rejects_paths_with_slash() {
696        assert!(!IntralinkResolver::is_intralink(&Link::new("foo/bar".to_owned())));
697        assert!(!IntralinkResolver::is_intralink(&Link::new("/abs/path".to_owned())));
698        assert!(!IntralinkResolver::is_intralink(&Link::new("./relative".to_owned())));
699        assert!(!IntralinkResolver::is_intralink(&Link::new("https://example.com".to_owned())));
700    }
701
702    #[test]
703    fn test_is_intralink_accepts_paths() {
704        assert!(IntralinkResolver::is_intralink(&Link::new("Foo".to_owned())));
705        assert!(IntralinkResolver::is_intralink(&Link::new("crate::Foo".to_owned())));
706        assert!(IntralinkResolver::is_intralink(&Link::new("::std::vec::Vec".to_owned())));
707        assert!(IntralinkResolver::is_intralink(&Link::new("type@crate::Foo".to_owned())));
708    }
709}