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
15pub 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 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 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 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 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 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 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 _ => {
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 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 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
560fn 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 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}