1use crate::PackageTarget;
2use crate::transform::intralinks::ItemPath;
3use crate::transform::intralinks::links::Link;
4use crate::transform::{IntralinkError, IntralinksConfig, IntralinksDocsRsConfig};
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-06-22";
17const EXPECTED_RUSTDOC_FORMAT_VERSION: u32 = 57;
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 =
287 existing_item_info.merge(item_info).expect("unmergeable item info");
288 })
289 .or_insert_with(|| item_info.clone());
290 }
291
292 let Some(item) = rustdoc_crate.index.get(&item_id) else {
293 return;
295 };
296
297 let inner_item_context = match item.inner {
298 ItemEnum::Trait(_) => ItemContext::Trait,
299 ItemEnum::Impl(_) => ItemContext::Impl,
300 _ => item_context,
301 };
302
303 for inner_item_id in child_item_ids(item) {
304 let inner_item_parent_kind = match item.name {
307 Some(_) => Some(item_info.kind),
308 None => item_info.parent_kind,
309 };
310
311 let inner_item_info = get_item_info(
312 inner_item_id,
313 &item_info.path,
314 inner_item_parent_kind,
315 inner_item_context,
316 rustdoc_crate,
317 );
318
319 if let Some(inner_item_info) = inner_item_info {
320 transitive_items(
321 inner_item_id,
322 &inner_item_info,
323 inner_item_context,
324 rustdoc_crate,
325 items_info,
326 );
327 }
328 }
329}
330
331pub struct IntralinkResolver<'a> {
332 link_url: HashMap<Link, String>,
333 config: &'a IntralinksDocsRsConfig,
334 package_name: &'a str,
335}
336
337impl<'a> IntralinkResolver<'a> {
338 pub fn new(package_name: &'a str, config: &'a IntralinksDocsRsConfig) -> IntralinkResolver<'a> {
339 IntralinkResolver { link_url: HashMap::new(), package_name, config }
340 }
341
342 fn url_segment(kind: ItemKind, name: &str) -> String {
343 match kind {
344 ItemKind::Module => format!("{name}/"),
345 ItemKind::Struct => format!("struct.{name}.html"),
346 ItemKind::StructField => format!("#structfield.{name}"),
347 ItemKind::Union => format!("union.{name}.html"),
348 ItemKind::Enum => format!("enum.{name}.html"),
349 ItemKind::Variant => format!("#variant.{name}"),
350 ItemKind::Function => format!("fn.{name}.html"),
351 ItemKind::Method => format!("#method.{name}"),
352 ItemKind::TyMethod => format!("#tymethod.{name}"),
353 ItemKind::TypeAlias => format!("type.{name}.html"),
354 ItemKind::Constant => format!("const.{name}.html"),
355 ItemKind::Trait => format!("trait.{name}.html"),
356 ItemKind::TraitAlias => format!("traitalias.{name}.html"),
357 ItemKind::Static => format!("static.{name}.html"),
358 ItemKind::Macro => format!("macro.{name}.html"),
359 ItemKind::ProcAttribute => format!("attr.{name}.html"),
360 ItemKind::ProcDerive => format!("derive.{name}.html"),
361 ItemKind::AssocConst => {
362 format!("#associatedconstant.{name}")
363 }
364 ItemKind::AssocType => format!("#associatedtype.{name}"),
365 ItemKind::Primitive => format!("primitive.{name}.html"),
366
367 ItemKind::Keyword
368 | ItemKind::ExternCrate
369 | ItemKind::Use
370 | ItemKind::Impl
371 | ItemKind::ExternType
372 | ItemKind::Attribute => {
373 unreachable!("items of kind {:?} cannot be intralinked to", kind);
374 }
375 }
376 }
377
378 fn is_stdlib_crate(external_crate: &ExternalCrate) -> bool {
379 external_crate
380 .html_root_url
381 .as_deref()
382 .is_some_and(|base_url| base_url.starts_with("https://doc.rust-lang.org/"))
383 }
384
385 fn make_url(base_url: &str, package_name: &str, version: &str, url_path: &str) -> String {
386 format!("{base_url}/{package_name}/{version}/{url_path}")
387 }
388
389 fn add(
390 &mut self,
391 link: Link,
392 item_info: &ItemInfo,
393 external_crates: &HashMap<u32, ExternalCrate>,
394 ) {
395 let docs_rs_base_url = self.config.docs_rs_base_url.as_deref().unwrap_or("https://docs.rs");
396
397 let path_segment_kind = |i: usize| match item_info.path.len() - i {
398 1 => item_info.kind,
399 2 => item_info.parent_kind.unwrap_or(ItemKind::Module),
400 _ => ItemKind::Module,
401 };
402 let url_path = item_info
403 .path
404 .segments()
405 .enumerate()
406 .map(|(i, segment)| (segment, path_segment_kind(i)))
407 .map(|(segment, item_kind)| IntralinkResolver::url_segment(item_kind, segment))
408 .join("");
409
410 let url = match item_info.crate_id {
411 0 => {
413 let version = self.config.docs_rs_version.as_deref().unwrap_or("latest");
414 let package_name = &self.package_name;
415
416 Self::make_url(docs_rs_base_url, package_name, version, &url_path)
417 }
418 _ => {
420 let Some(external_crate) = external_crates.get(&item_info.crate_id) else {
421 return;
422 };
423
424 match external_crate.html_root_url.as_deref() {
425 Some(base_url) => {
426 let base_url = match Self::is_stdlib_crate(external_crate) {
427 true => {
428 base_url
431 .strip_suffix("/nightly/")
432 .map_or_else(|| base_url.to_owned(), |p| format!("{p}/stable/"))
433 }
434 false => base_url.to_owned(),
435 };
436
437 format!("{base_url}{url_path}")
438 }
439 None => {
440 let crate_name = &external_crate.name;
441
442 Self::make_url(docs_rs_base_url, crate_name, "latest", &url_path)
451 }
452 }
453 }
454 };
455
456 self.link_url.insert(link, url);
457 }
458
459 pub fn resolve_link(&self, link: &Link) -> Option<&str> {
460 self.link_url.get(link).map(String::as_str)
461 }
462
463 pub fn is_intralink(link: &Link) -> bool {
464 let has_lone_colon = || link.raw_link.replace("::", "").contains(':');
465
466 !link.symbol().is_empty() && !link.raw_link.contains('/') && !has_lone_colon()
467 }
468}
469
470fn run_rustdoc(
471 package_target: &PackageTarget,
472 workspace_package: Option<&str>,
473 manifest_path: &PathBuf,
474 config: &IntralinksConfig,
475) -> Result<Crate, IntralinkError> {
476 let rustdoc_json_path: PathBuf = {
477 let target: rustdoc_json::PackageTarget = match package_target {
478 PackageTarget::Bin { crate_name } => {
479 rustdoc_json::PackageTarget::Bin(crate_name.clone())
480 }
481 PackageTarget::Lib => rustdoc_json::PackageTarget::Lib,
482 };
483 let mut stderr = Vec::new();
484
485 let toolchain = match is_expected_rust_toolchain_installed()? {
486 true => EXPECTED_RUST_TOOLCHAIN,
487 false => {
488 return Err(IntralinkError::RustToolchainNotInstalled {
489 expected: EXPECTED_RUST_TOOLCHAIN,
490 });
491 }
492 };
493
494 let mut builder = rustdoc_json::Builder::default()
495 .toolchain(toolchain)
496 .manifest_path(manifest_path)
497 .document_private_items(true)
498 .all_features(config.all_features.unwrap_or_default())
499 .features(config.features.clone().unwrap_or_default())
500 .no_default_features(config.no_default_features.unwrap_or_default())
501 .quiet(true)
502 .color(rustdoc_json::Color::Never)
503 .package_target(target);
504
505 if let Some(package) = workspace_package {
506 builder = builder.package(package);
507 }
508
509 let result = builder.build_with_captured_output(std::io::sink(), &mut stderr);
510
511 result.map_err(|error| match error {
512 BuildError::BuildRustdocJsonError => match stderr.is_empty() {
513 true => IntralinkError::BuildRustdocError {
514 stderr: "Weirdly, rustdoc did not write anything to stderr".to_owned(),
515 },
516 false => IntralinkError::BuildRustdocError {
517 stderr: String::from_utf8_lossy(&stderr).into_owned(),
518 },
519 },
520 e => IntralinkError::RustdocError { error: e },
521 })?
522 };
523
524 let rustdoc_crate = crate_from_file(&rustdoc_json_path)?;
525
526 match rustdoc_crate.format_version {
527 EXPECTED_RUSTDOC_FORMAT_VERSION => Ok(rustdoc_crate),
528 format_version => Err(IntralinkError::UnsupportedRustdocFormatVersion {
529 version: format_version,
530 expected_version: EXPECTED_RUSTDOC_FORMAT_VERSION,
531 }),
532 }
533}
534
535fn items_info(rustdoc_crate: &Crate) -> HashMap<ItemId, ItemInfo<'_>> {
536 let mut items_info: HashMap<ItemId, ItemInfo<'_>> =
537 HashMap::with_capacity(rustdoc_crate.index.len());
538
539 for (&item_id, item_summary) in &rustdoc_crate.paths {
540 let item_info = ItemInfo::from(item_summary, None, ItemContext::Normal);
541
542 transitive_items(item_id, &item_info, ItemContext::Normal, rustdoc_crate, &mut items_info);
543 }
544
545 items_info
546}
547
548pub fn create_intralink_resolver<'a>(
549 package_name: &'a str,
550 package_target: &PackageTarget,
551 workspace_package: Option<&str>,
552 manifest_path: &PathBuf,
553 config: &'a IntralinksConfig,
554) -> Result<IntralinkResolver<'a>, IntralinkError> {
555 let rustdoc_crate = run_rustdoc(package_target, workspace_package, manifest_path, config)?;
556
557 let items_info: HashMap<ItemId, ItemInfo<'_>> = items_info(&rustdoc_crate);
558 let links_items_id = crate_rustdoc_intralinks(&rustdoc_crate);
559 let mut intralink_resolver = IntralinkResolver::new(package_name, &config.docs_rs);
560
561 for (link, item_id) in links_items_id {
562 let link = Link::new(link.clone());
563 let Some(item_info) = items_info.get(item_id) else {
564 continue;
566 };
567
568 intralink_resolver.add(link, item_info, &rustdoc_crate.external_crates);
569 }
570
571 Ok(intralink_resolver)
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use pretty_assertions::assert_eq;
578
579 #[test]
580 fn test_rustdoc_format_supported_version() {
581 assert_eq!(rustdoc_types::FORMAT_VERSION, EXPECTED_RUSTDOC_FORMAT_VERSION);
582 }
583
584 fn make_item_info(
585 crate_id: u32,
586 path: &'static [&'static str],
587 kind: ItemKind,
588 parent_kind: Option<ItemKind>,
589 ) -> ItemInfo<'static> {
590 let segments: &'static [String] = Box::leak(
591 path.iter().map(|&s| s.to_owned()).collect::<Vec<String>>().into_boxed_slice(),
592 );
593
594 ItemInfo::new(crate_id, ItemPath::new(segments), kind, parent_kind)
595 }
596
597 #[test]
598 fn test_item_info_merge_identical() {
599 let a = make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
600 let b = a.clone();
601
602 let merged = a.merge(&b).expect("identical items should merge");
603
604 assert_eq!(merged.crate_id, 0);
605 assert_eq!(merged.kind, ItemKind::Struct);
606 assert_eq!(merged.parent_kind, Some(ItemKind::Module));
607 }
608
609 #[test]
610 fn test_item_info_merge_fills_missing_parent_kind() {
611 let with_parent =
612 make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
613 let without_parent = make_item_info(0, &["foo", "Bar"], ItemKind::Struct, None);
614
615 let merged_a = with_parent.merge(&without_parent).expect("compatible parent_kinds");
616 let merged_b = without_parent.merge(&with_parent).expect("compatible parent_kinds");
617
618 assert_eq!(merged_a.parent_kind, Some(ItemKind::Module));
619 assert_eq!(merged_b.parent_kind, Some(ItemKind::Module));
620 }
621
622 #[test]
623 fn test_item_info_merge_rejects_mismatch() {
624 let base = make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
625
626 let different_crate =
627 make_item_info(1, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
628 let different_path =
629 make_item_info(0, &["other", "Bar"], ItemKind::Struct, Some(ItemKind::Module));
630 let different_kind =
631 make_item_info(0, &["foo", "Bar"], ItemKind::Enum, Some(ItemKind::Module));
632 let different_parent =
633 make_item_info(0, &["foo", "Bar"], ItemKind::Struct, Some(ItemKind::Struct));
634
635 assert!(base.merge(&different_crate).is_none());
636 assert!(base.merge(&different_path).is_none());
637 assert!(base.merge(&different_kind).is_none());
638 assert!(base.merge(&different_parent).is_none());
639 }
640
641 #[test]
642 fn test_is_intralink_rejects_paths_with_slash() {
643 assert!(!IntralinkResolver::is_intralink(&Link::new("foo/bar".to_owned())));
644 assert!(!IntralinkResolver::is_intralink(&Link::new("/abs/path".to_owned())));
645 assert!(!IntralinkResolver::is_intralink(&Link::new("./relative".to_owned())));
646 assert!(!IntralinkResolver::is_intralink(&Link::new("https://example.com".to_owned())));
647 }
648
649 #[test]
650 fn test_is_intralink_accepts_paths() {
651 assert!(IntralinkResolver::is_intralink(&Link::new("Foo".to_owned())));
652 assert!(IntralinkResolver::is_intralink(&Link::new("crate::Foo".to_owned())));
653 assert!(IntralinkResolver::is_intralink(&Link::new("::std::vec::Vec".to_owned())));
654 assert!(IntralinkResolver::is_intralink(&Link::new("type@crate::Foo".to_owned())));
655 }
656}