1use crate::Doc;
2use crate::transform::DocTransform;
3use crate::transform::intralinks::links::{
4 Link, MarkdownLink, markdown_link_iterator, markdown_reference_link_definition_iterator,
5};
6use module_walker::walk_module_file;
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9use std::fmt::Write;
10use std::hash::{Hash, Hasher};
11use std::path::{Path, PathBuf};
12use std::rc::Rc;
13use syn::{Item, ItemMod};
14use thiserror::Error;
15use unicase::UniCase;
16
17mod links;
18mod module_walker;
19
20#[derive(Error, Debug)]
21pub enum IntralinkError {
22 #[error("IO error: {0}")]
23 IOError(std::io::Error),
24 #[error("failed to analyzing code: {0}")]
25 AstWalkError(module_walker::ModuleWalkError),
26 #[error("failed to load standard library: {0}")]
27 LoadStdLibError(String),
28}
29
30impl From<std::io::Error> for IntralinkError {
31 fn from(err: std::io::Error) -> Self {
32 IntralinkError::IOError(err)
33 }
34}
35
36impl From<module_walker::ModuleWalkError> for IntralinkError {
37 fn from(err: module_walker::ModuleWalkError) -> Self {
38 IntralinkError::AstWalkError(err)
39 }
40}
41
42#[derive(Default, Debug, PartialEq, Eq, Clone)]
43pub struct IntralinksDocsRsConfig {
44 pub docs_rs_base_url: Option<String>,
45 pub docs_rs_version: Option<String>,
46}
47
48#[derive(Default, Debug, PartialEq, Eq, Clone)]
49pub struct IntralinksConfig {
50 pub docs_rs: IntralinksDocsRsConfig,
51 pub strip_links: Option<bool>,
52}
53
54pub struct DocTransformIntralinks<F> {
55 crate_name: String,
56 entrypoint: PathBuf,
57 emit_warning: F,
58 config: IntralinksConfig,
59}
60
61impl<F> DocTransformIntralinks<F>
62where
63 F: Fn(&str),
64{
65 pub fn new(
66 crate_name: impl Into<String>,
67 entrypoint: impl AsRef<Path>,
68 emit_warning: F,
69 config: Option<IntralinksConfig>,
70 ) -> DocTransformIntralinks<F> {
71 DocTransformIntralinks {
72 crate_name: crate_name.into(),
73 entrypoint: entrypoint.as_ref().to_path_buf(),
74 emit_warning,
75 config: config.unwrap_or_default(),
76 }
77 }
78}
79
80impl<F> DocTransform for DocTransformIntralinks<F>
81where
82 F: Fn(&str),
83{
84 type E = IntralinkError;
85
86 fn transform(&self, doc: &Doc) -> Result<Doc, IntralinkError> {
87 let symbols: HashSet<ItemPath> = extract_markdown_intralink_symbols(doc);
88
89 if symbols.is_empty() {
91 return Ok(doc.clone());
92 }
93
94 let symbols_type = match self.config.strip_links.unwrap_or(false) {
96 false => load_symbols_type(&self.entrypoint, &symbols, &self.emit_warning)?,
97 true => HashMap::new(),
98 };
99
100 let doc =
101 rewrite_links(doc, &symbols_type, &self.crate_name, &self.emit_warning, &self.config);
102
103 Ok(doc)
104 }
105}
106
107fn rewrite_links(
108 doc: &Doc,
109 symbols_type: &HashMap<ItemPath, SymbolType>,
110 crate_name: &str,
111 emit_warning: &impl Fn(&str),
112 config: &IntralinksConfig,
113) -> Doc {
114 let RewriteReferenceLinksResult { doc, reference_links_to_remove } =
115 rewrite_reference_links_definitions(doc, symbols_type, crate_name, emit_warning, config);
116
117 rewrite_markdown_links(
120 &doc,
121 symbols_type,
122 crate_name,
123 emit_warning,
124 config,
125 &reference_links_to_remove,
126 )
127}
128
129#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
130pub enum ItemPathAnchor {
131 Root,
133 Crate,
135}
136
137#[derive(Clone)]
139pub struct ItemPath {
140 pub anchor: ItemPathAnchor,
141
142 path_shared: Rc<Vec<String>>,
145 path_end: usize,
146}
147
148impl ItemPath {
149 fn new(anchor: ItemPathAnchor) -> ItemPath {
150 ItemPath { anchor, path_shared: Rc::new(Vec::new()), path_end: 0 }
151 }
152
153 fn root(crate_name: &str) -> ItemPath {
154 ItemPath::new(ItemPathAnchor::Root).join(&crate_name)
155 }
156
157 fn from_string(s: &str) -> Option<ItemPath> {
158 let anchor;
159 let rest;
160
161 if let Some(r) = s.strip_prefix("::") {
162 anchor = ItemPathAnchor::Root;
163 rest = r;
164 } else if s == "crate" {
165 return Some(ItemPath::new(ItemPathAnchor::Crate));
166 } else {
167 let r = s.strip_prefix("crate::")?;
168 anchor = ItemPathAnchor::Crate;
169 rest = r;
170 }
171
172 if rest.is_empty() {
173 return None;
174 }
175
176 let path: Rc<Vec<String>> = Rc::new(rest.split("::").map(str::to_owned).collect());
177
178 Some(ItemPath { anchor, path_end: path.len(), path_shared: path })
179 }
180
181 fn path_components(&self) -> impl Iterator<Item = &str> {
182 self.path_shared[0..self.path_end].iter().map(String::as_str)
183 }
184
185 fn is_toplevel(&self) -> bool {
186 match self.anchor {
187 ItemPathAnchor::Root => self.path_end <= 1,
188 ItemPathAnchor::Crate => self.path_end == 0,
189 }
190 }
191
192 fn parent(mut self) -> Option<ItemPath> {
193 match self.is_toplevel() {
194 true => None,
195 false => {
196 self.path_end -= 1;
197 Some(self)
198 }
199 }
200 }
201
202 fn name(&self) -> Option<&str> {
203 self.path_end.checked_sub(1).and_then(|i| self.path_shared.get(i)).map(String::as_str)
204 }
205
206 fn join(mut self, s: &impl ToString) -> ItemPath {
207 let path = Rc::make_mut(&mut self.path_shared);
208 path.truncate(self.path_end);
209 path.push(s.to_string());
210 self.path_end += 1;
211 self
212 }
213
214 fn all_ancestors(&self) -> impl Iterator<Item = ItemPath> + use<> {
215 let first_ancestor = self.clone().parent();
216
217 std::iter::successors(first_ancestor, |ancestor| ancestor.clone().parent())
218 }
219}
220
221impl PartialEq for ItemPath {
222 fn eq(&self, other: &Self) -> bool {
223 self.anchor == other.anchor && self.path_components().eq(other.path_components())
224 }
225}
226
227impl Eq for ItemPath {}
228
229impl Hash for ItemPath {
230 fn hash<H: Hasher>(&self, state: &mut H) {
231 self.anchor.hash(state);
232 self.path_components().for_each(|c| c.hash(state));
233 }
234}
235
236impl fmt::Display for ItemPath {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
238 match self.anchor {
239 ItemPathAnchor::Root => (),
240 ItemPathAnchor::Crate => f.write_str("crate")?,
241 }
242
243 for s in self.path_components() {
244 f.write_str("::")?;
245 f.write_str(s)?;
246 }
247
248 Ok(())
249 }
250}
251
252impl fmt::Debug for ItemPath {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
254 fmt::Display::fmt(self, f)
255 }
256}
257
258#[derive(Copy, Clone, Debug, PartialEq, Eq)]
259pub enum ImplSymbolType {
260 Method,
261 Const,
262 Type,
263}
264
265#[derive(Copy, Clone, Debug, PartialEq, Eq)]
266pub enum SymbolType {
267 Crate,
268 Struct,
269 Trait,
270 Enum,
271 Union,
272 Type,
273 Mod,
274 Macro,
275 Const,
276 Fn,
277 Static,
278 ImplItem(ImplSymbolType),
279}
280
281impl SymbolType {
282 fn get_module_path(self, path: &ItemPath) -> Option<ItemPath> {
287 match self {
288 SymbolType::Crate => {
289 assert!(path.is_toplevel(), "a crate should always be in a toplevel path");
290 None
291 }
292 SymbolType::Struct
293 | SymbolType::Trait
294 | SymbolType::Enum
295 | SymbolType::Union
296 | SymbolType::Type
297 | SymbolType::Mod
298 | SymbolType::Macro
299 | SymbolType::Const
300 | SymbolType::Fn
301 | SymbolType::Static => {
302 let p = path.clone().parent().unwrap_or_else(|| {
303 panic!("item {path} of type {self:?} should have a parent module")
304 });
305 Some(p)
306 }
307 SymbolType::ImplItem(_) => {
308 let p = path
309 .clone()
310 .parent()
311 .unwrap_or_else(|| {
312 panic!("item {path} of type {self:?} should have a parent type")
313 })
314 .parent()
315 .unwrap_or_else(|| {
316 panic!("item {path} of type {self:?} should have a parent module")
317 });
318 Some(p)
319 }
320 }
321 }
322}
323
324fn symbols_type_impl_block(
325 module: &ItemPath,
326 impl_block: &syn::ItemImpl,
327) -> Vec<(ItemPath, SymbolType)> {
328 use syn::{ImplItem, Type, TypePath};
329
330 if let Type::Path(TypePath { qself: None, path }) = &*impl_block.self_ty {
331 if let Some(self_ident) = path.get_ident().map(ToString::to_string) {
332 let self_path = module.clone().join(&self_ident);
333
334 return impl_block
335 .items
336 .iter()
337 .filter_map(|item| match item {
338 ImplItem::Fn(m) => {
339 let ident = m.sig.ident.to_string();
340
341 Some((ident, ImplSymbolType::Method))
342 }
343 ImplItem::Const(c) => {
344 let ident = c.ident.to_string();
345
346 Some((ident, ImplSymbolType::Const))
347 }
348 ImplItem::Type(t) => {
349 let ident = t.ident.to_string();
350
351 Some((ident, ImplSymbolType::Type))
352 }
353 _ => None,
354 })
355 .map(|(ident, tpy)| (self_path.clone().join(&ident), SymbolType::ImplItem(tpy)))
356 .collect();
357 }
358 }
359
360 Vec::new()
361}
362
363fn item_symbols_type(module: &ItemPath, item: &Item) -> Vec<(ItemPath, SymbolType)> {
364 let item_path = |ident: &syn::Ident| module.clone().join(ident);
365
366 let (path, symbol_type) = match item {
367 Item::Enum(e) => (item_path(&e.ident), SymbolType::Enum),
368 Item::Struct(s) => (item_path(&s.ident), SymbolType::Struct),
369 Item::Trait(t) => (item_path(&t.ident), SymbolType::Trait),
370 Item::Union(u) => (item_path(&u.ident), SymbolType::Union),
371 Item::Type(t) => (item_path(&t.ident), SymbolType::Type),
372 Item::Mod(m) => (item_path(&m.ident), SymbolType::Mod),
373 Item::Macro(syn::ItemMacro { ident: Some(ident), .. }) => {
374 (item_path(ident), SymbolType::Macro)
375 }
376 Item::Const(c) => (item_path(&c.ident), SymbolType::Const),
377 Item::Fn(f) => (item_path(&f.sig.ident), SymbolType::Fn),
378 Item::Static(s) => (item_path(&s.ident), SymbolType::Static),
379 Item::Impl(impl_block) => {
380 return symbols_type_impl_block(module, impl_block);
381 }
382
383 _ => return Vec::new(),
384 };
385
386 vec![(path, symbol_type)]
387}
388
389fn is_cfg_test(attribute: &syn::Attribute) -> bool {
390 let test_attribute: syn::Attribute = syn::parse_quote!(#[cfg(test)]);
391
392 *attribute == test_attribute
393}
394
395fn visit_module_item(
396 save_symbol: impl Fn(&ItemPath) -> bool,
397 symbols_type: &mut HashMap<ItemPath, SymbolType>,
398 module: &ItemPath,
399 item: &Item,
400) {
401 for (symbol, symbol_type) in item_symbols_type(module, item) {
402 if save_symbol(&symbol) {
403 symbols_type.insert(symbol, symbol_type);
404 }
405 }
406}
407
408fn check_explore_module(
410 should_explore_module: impl Fn(&ItemPath) -> bool,
411 modules_visited: &mut HashSet<ItemPath>,
412 mod_symbol: &ItemPath,
413 mod_item: &ItemMod,
414) -> bool {
415 if modules_visited.contains(mod_symbol) {
426 return false;
427 }
428
429 if mod_item.attrs.iter().any(is_cfg_test) {
432 return false;
433 }
434
435 let explore = should_explore_module(mod_symbol);
436
437 if explore {
438 modules_visited.insert(mod_symbol.clone());
439 }
440
441 explore
442}
443
444fn explore_crate<P: AsRef<Path>>(
445 file: P,
446 crate_symbol: &ItemPath,
447 symbols: &HashSet<ItemPath>,
448 paths_to_explore: &HashSet<ItemPath>,
449 symbols_type: &mut HashMap<ItemPath, SymbolType>,
450 emit_warning: &impl Fn(&str),
451) -> Result<(), module_walker::ModuleWalkError> {
452 let mut modules_visited: HashSet<ItemPath> = HashSet::new();
453
454 symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
456
457 let mut visit = |module: &ItemPath, item: &Item| {
458 let save_symbol = |symbol: &ItemPath| {
459 symbols.contains(symbol) || paths_to_explore.contains(symbol)
463 };
464
465 visit_module_item(save_symbol, symbols_type, module, item);
466 };
467
468 let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
469 check_explore_module(
470 |mod_symbol| paths_to_explore.contains(mod_symbol),
471 &mut modules_visited,
472 mod_symbol,
473 mod_item,
474 )
475 };
476
477 walk_module_file(file, crate_symbol, &mut visit, &mut explore_module, emit_warning)
478}
479
480fn load_symbols_type<P: AsRef<Path>>(
481 entry_point: P,
482 symbols: &HashSet<ItemPath>,
483 emit_warning: &impl Fn(&str),
484) -> Result<HashMap<ItemPath, SymbolType>, IntralinkError> {
485 let paths_to_explore: HashSet<ItemPath> = all_ancestor_paths(symbols.iter());
486 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
487
488 let std_lib_crates = match references_standard_library(symbols) {
490 true => get_standard_libraries()?,
491 false => Vec::new(),
492 };
493
494 for Crate { name, entrypoint } in std_lib_crates {
495 explore_crate(
496 entrypoint,
497 &ItemPath::root(&name),
498 symbols,
499 &paths_to_explore,
500 &mut symbols_type,
501 emit_warning,
502 )?;
503 }
504
505 explore_crate(
506 entry_point,
507 &ItemPath::new(ItemPathAnchor::Crate),
508 symbols,
509 &paths_to_explore,
510 &mut symbols_type,
511 emit_warning,
512 )?;
513
514 Ok(symbols_type)
515}
516
517fn all_ancestor_paths<'a>(symbols: impl Iterator<Item = &'a ItemPath>) -> HashSet<ItemPath> {
521 symbols.into_iter().flat_map(ItemPath::all_ancestors).collect()
522}
523
524fn extract_markdown_intralink_symbols(doc: &Doc) -> HashSet<ItemPath> {
525 let item_paths_inline_links =
526 markdown_link_iterator(&doc.markdown).items().filter_map(|l| match l {
527 MarkdownLink::Inline { link: inline_link } => inline_link.link.link_as_item_path(),
528 MarkdownLink::Reference { .. } => None,
529 });
530
531 let item_paths_reference_link_def = markdown_reference_link_definition_iterator(&doc.markdown)
532 .items()
533 .filter_map(|l| l.link.link_as_item_path());
534
535 item_paths_inline_links.chain(item_paths_reference_link_def).collect()
536}
537
538fn documentation_url(
542 item_path: &ItemPath,
543 symbols_type: &HashMap<ItemPath, SymbolType>,
544 crate_name: &str,
545 fragment: Option<&str>,
546 config: &IntralinksDocsRsConfig,
547) -> Option<String> {
548 let package_name = crate_name.replace('-', "_");
549 let typ = *symbols_type.get(item_path)?;
550
551 let mut link = match item_path.anchor {
552 ItemPathAnchor::Root => {
553 let std_crate_name =
554 item_path.path_components().next().expect("a root path should not be empty");
555 format!("https://doc.rust-lang.org/stable/{std_crate_name}/")
556 }
557 ItemPathAnchor::Crate => {
558 let base_url =
559 config.docs_rs_base_url.as_ref().map_or("https://docs.rs", String::as_str);
560 let version = config.docs_rs_version.as_ref().map_or("latest", String::as_str);
561
562 format!("{base_url}/{crate_name}/{version}/{package_name}/")
563 }
564 };
565
566 if typ == SymbolType::Crate {
567 return Some(format!("{}{}", link, fragment.unwrap_or("")));
568 }
569
570 let skip_components = match item_path.anchor {
571 ItemPathAnchor::Root => 1,
572 ItemPathAnchor::Crate => 0,
573 };
574
575 let module_path = typ.get_module_path(item_path).expect("item should belong to a module");
576
577 for s in module_path.path_components().skip(skip_components) {
578 link.push_str(s);
579 link.push('/');
580 }
581
582 let name =
583 item_path.name().unwrap_or_else(|| panic!("failed to get last component of {item_path}"));
584
585 match typ {
586 SymbolType::Crate => unreachable!(),
587 SymbolType::Struct => write!(&mut link, "struct.{name}.html"),
588 SymbolType::Trait => write!(&mut link, "trait.{name}.html"),
589 SymbolType::Enum => write!(&mut link, "enum.{name}.html"),
590 SymbolType::Union => write!(&mut link, "union.{name}.html"),
591 SymbolType::Type => write!(&mut link, "type.{name}.html"),
592 SymbolType::Mod => write!(&mut link, "{name}/"),
593 SymbolType::Macro => write!(&mut link, "macro.{name}.html"),
594 SymbolType::Const => write!(&mut link, "const.{name}.html"),
595 SymbolType::Fn => write!(&mut link, "fn.{name}.html"),
596 SymbolType::Static => write!(&mut link, "static.{name}.html"),
597 SymbolType::ImplItem(typ) => {
598 let parent_path = item_path
599 .clone()
600 .parent()
601 .unwrap_or_else(|| panic!("item {item_path} should always have a parent"));
602
603 let link = documentation_url(
604 &parent_path,
605 symbols_type,
606 crate_name,
607 None,
609 config,
610 )?;
611
612 let impl_item_fragment_str = match typ {
613 ImplSymbolType::Method => "method",
614 ImplSymbolType::Const => "associatedconstant",
615 ImplSymbolType::Type => "associatedtype",
616 };
617
618 return Some(format!("{link}#{impl_item_fragment_str}.{name}"));
619 }
620 }
621 .expect("this should never fail");
622
623 Some(format!("{}{}", link, fragment.unwrap_or("")))
624}
625
626enum MarkdownLinkAction {
627 Link(Link),
628 Preserve,
629 Strip,
630}
631
632fn markdown_link(
633 link: &Link,
634 symbols_type: &HashMap<ItemPath, SymbolType>,
635 crate_name: &str,
636 emit_warning: &impl Fn(&str),
637 config: &IntralinksConfig,
638) -> MarkdownLinkAction {
639 match link.link_as_item_path() {
640 Some(symbol) => {
641 let link = documentation_url(
642 &symbol,
643 symbols_type,
644 crate_name,
645 link.link_fragment(),
646 &config.docs_rs,
647 );
648
649 match link {
650 Some(l) => MarkdownLinkAction::Link(l.into()),
651 None => {
652 emit_warning(&format!("Could not resolve definition of `{symbol}`."));
653
654 MarkdownLinkAction::Strip
656 }
657 }
658 }
659 None => MarkdownLinkAction::Preserve,
660 }
661}
662
663fn rewrite_markdown_links(
664 doc: &Doc,
665 symbols_type: &HashMap<ItemPath, SymbolType>,
666 crate_name: &str,
667 emit_warning: &impl Fn(&str),
668 config: &IntralinksConfig,
669 reference_links_to_remove: &HashSet<UniCase<String>>,
670) -> Doc {
671 use crate::utils::ItemOrOther;
672
673 let strip_links = config.strip_links.unwrap_or(false);
674 let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
675
676 for item_or_other in markdown_link_iterator(&doc.markdown).complete() {
677 match item_or_other {
678 ItemOrOther::Item(MarkdownLink::Inline { link: inline_link }) => {
679 let markdown_link: MarkdownLinkAction = match strip_links {
680 false => markdown_link(
681 &inline_link.link,
682 symbols_type,
683 crate_name,
684 emit_warning,
685 config,
686 ),
687 true => match inline_link.link.link_as_item_path() {
688 None => MarkdownLinkAction::Preserve,
689 Some(_) => MarkdownLinkAction::Strip,
690 },
691 };
692
693 match markdown_link {
694 MarkdownLinkAction::Link(markdown_link) => {
695 new_doc.push_str(&inline_link.with_link(markdown_link).to_string());
696 }
697 MarkdownLinkAction::Preserve => {
698 new_doc.push_str(&inline_link.to_string());
699 }
700 MarkdownLinkAction::Strip => {
701 new_doc.push_str(&inline_link.text);
702 }
703 }
704 }
705 ItemOrOther::Item(MarkdownLink::Reference { link }) => {
706 match reference_links_to_remove.contains(link.label()) {
707 true => new_doc.push_str(link.text()),
708 false => new_doc.push_str(&link.to_string()),
709 }
710 }
711 ItemOrOther::Other(other) => {
712 new_doc.push_str(other);
713 }
714 }
715 }
716
717 Doc::from_str(new_doc)
718}
719
720struct RewriteReferenceLinksResult {
721 doc: Doc,
722 reference_links_to_remove: HashSet<UniCase<String>>,
723}
724
725fn rewrite_reference_links_definitions(
726 doc: &Doc,
727 symbols_type: &HashMap<ItemPath, SymbolType>,
728 crate_name: &str,
729 emit_warning: &impl Fn(&str),
730 config: &IntralinksConfig,
731) -> RewriteReferenceLinksResult {
732 use crate::utils::ItemOrOther;
733 let mut reference_links_to_remove = HashSet::new();
734 let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
735 let mut skip_next_newline = false;
736 let strip_links = config.strip_links.unwrap_or(false);
737
738 let iter = markdown_reference_link_definition_iterator(&doc.markdown);
739
740 for item_or_other in iter.complete() {
741 match item_or_other {
742 ItemOrOther::Item(link_ref_def) => {
743 let markdown_link: MarkdownLinkAction = match strip_links {
744 false => markdown_link(
745 &link_ref_def.link,
746 symbols_type,
747 crate_name,
748 emit_warning,
749 config,
750 ),
751 true => match link_ref_def.link.link_as_item_path() {
752 None => MarkdownLinkAction::Preserve,
753 Some(_) => MarkdownLinkAction::Strip,
754 },
755 };
756
757 match markdown_link {
758 MarkdownLinkAction::Link(link) => {
759 new_doc.push_str(&link_ref_def.with_link(link).to_string());
760 }
761 MarkdownLinkAction::Preserve => {
762 new_doc.push_str(&link_ref_def.to_string());
763 }
764 MarkdownLinkAction::Strip => {
765 reference_links_to_remove.insert(link_ref_def.label);
767 skip_next_newline = true;
768 }
769 }
770 }
771 ItemOrOther::Other(other) => {
772 let other = match skip_next_newline {
773 true => {
774 skip_next_newline = false;
775 let next_index = other
776 .chars()
777 .enumerate()
778 .skip_while(|(_, c)| c.is_whitespace() && *c != '\n')
779 .skip(1)
780 .map(|(i, _)| i)
781 .next();
782
783 next_index.and_then(|i| other.get(i..)).unwrap_or("")
784 }
785 false => other,
786 };
787 new_doc.push_str(other);
788 }
789 }
790 }
791
792 RewriteReferenceLinksResult { doc: Doc::from_str(new_doc), reference_links_to_remove }
793}
794
795fn get_rustc_sysroot_libraries_dir() -> Result<PathBuf, IntralinkError> {
796 use std::process::Command;
797
798 let output = Command::new("rustc")
799 .args(["--print=sysroot"])
800 .output()
801 .map_err(|e| IntralinkError::LoadStdLibError(format!("failed to run rustc: {e}")))?;
802
803 let s = String::from_utf8(output.stdout).expect("unexpected output from rustc");
804 let sysroot = PathBuf::from(s.trim());
805 let src_path = sysroot.join("lib").join("rustlib").join("src").join("rust").join("library");
806
807 match src_path.is_dir() {
808 false => Err(IntralinkError::LoadStdLibError(format!(
809 "Cannot find rust standard library in \"{}\"",
810 src_path.display()
811 ))),
812 true => Ok(src_path),
813 }
814}
815
816#[derive(Debug)]
817struct Crate {
818 name: String,
819 entrypoint: PathBuf,
820}
821
822fn references_standard_library(symbols: &HashSet<ItemPath>) -> bool {
823 symbols.iter().any(|symbol| symbol.anchor == ItemPathAnchor::Root)
825}
826
827fn get_standard_libraries() -> Result<Vec<Crate>, IntralinkError> {
828 let libraries_dir = get_rustc_sysroot_libraries_dir()?;
829 let mut std_libs = Vec::with_capacity(64);
830
831 for entry in std::fs::read_dir(libraries_dir)? {
832 let entry = entry?;
833 let project_dir_path = entry.path();
834 let cargo_manifest_path = project_dir_path.join("Cargo.toml");
835 let lib_entrypoint = project_dir_path.join("src").join("lib.rs");
836
837 if cargo_manifest_path.is_file() && lib_entrypoint.is_file() {
838 let crate_name =
839 crate::project_package_name(&cargo_manifest_path).ok_or_else(|| {
840 IntralinkError::LoadStdLibError(format!(
841 "failed to load manifest in \"{}\"",
842 cargo_manifest_path.display()
843 ))
844 })?;
845 let crate_info = Crate { name: crate_name, entrypoint: lib_entrypoint };
846
847 std_libs.push(crate_info);
848 }
849 }
850
851 Ok(std_libs)
852}
853
854#[allow(clippy::too_many_lines)]
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use indoc::indoc;
859 use module_walker::walk_module_items;
860 use std::cell::RefCell;
861
862 fn item_path(id: &str) -> ItemPath {
863 ItemPath::from_string(id).unwrap()
864 }
865
866 #[test]
867 fn test_item_path_is_toplevel() {
868 assert!(!item_path("crate::baz::mumble").is_toplevel());
869 assert!(!item_path("::std::baz::mumble").is_toplevel());
870 assert!(!item_path("crate::baz").is_toplevel());
871 assert!(!item_path("::std::baz").is_toplevel());
872 assert!(item_path("crate").is_toplevel());
873 assert!(item_path("::std").is_toplevel());
874 }
875
876 #[test]
877 fn test_item_path_parent() {
878 assert_eq!(item_path("crate::baz::mumble").parent(), Some(item_path("crate::baz")));
879 assert_eq!(item_path("::std::baz::mumble").parent(), Some(item_path("::std::baz")));
880 assert_eq!(item_path("crate::baz").parent(), Some(item_path("crate")));
881 assert_eq!(item_path("::std::baz").parent(), Some(item_path("::std")));
882 assert_eq!(item_path("crate").parent(), None);
883 assert_eq!(item_path("::std").parent(), None);
884 }
885
886 #[test]
887 fn test_item_path_join() {
888 assert_eq!(item_path("crate::foo").join(&"bar"), item_path("crate::foo::bar"),);
889 assert_eq!(item_path("::std::foo").join(&"bar"), item_path("::std::foo::bar"),);
890
891 assert_eq!(
892 item_path("::std::foo::bar").parent().unwrap().join(&"baz"),
893 item_path("::std::foo::baz"),
894 );
895 }
896
897 #[test]
898 fn test_all_ancestor_paths() {
899 let symbols = [
900 item_path("crate::foo::bar::baz"),
901 item_path("crate::baz::mumble"),
902 item_path("::std::vec::Vec"),
903 ];
904 let expected: HashSet<ItemPath> = [
905 item_path("crate"),
906 item_path("crate::foo"),
907 item_path("crate::foo::bar"),
908 item_path("crate::baz"),
909 item_path("::std"),
910 item_path("::std::vec"),
911 ]
912 .into_iter()
913 .collect();
914
915 assert_eq!(all_ancestor_paths(symbols.iter()), expected);
916 }
917
918 fn explore_crate(
919 ast: &[Item],
920 dir: &Path,
921 crate_symbol: &ItemPath,
922 should_explore_module: impl Fn(&ItemPath) -> bool,
923 symbols_type: &mut HashMap<ItemPath, SymbolType>,
924 emit_warning: impl Fn(&str),
925 ) {
926 let mut modules_visited: HashSet<ItemPath> = HashSet::new();
927
928 symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
929
930 let mut visit = |module: &ItemPath, item: &Item| {
931 visit_module_item(|_| true, symbols_type, module, item);
932 };
933
934 let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
935 check_explore_module(&should_explore_module, &mut modules_visited, mod_symbol, mod_item)
936 };
937
938 walk_module_items(ast, dir, crate_symbol, &mut visit, &mut explore_module, &emit_warning)
939 .ok()
940 .unwrap();
941 }
942
943 #[test]
944 fn test_walk_module_and_symbols_type() {
945 let module_skip: ItemPath = item_path("crate::skip");
946
947 let source = indoc! { "
948 struct AStruct {}
949
950 mod skip {
951 struct Skip {}
952 }
953
954 mod a {
955 mod b {
956 trait ATrait {}
957 }
958
959 struct FooStruct {}
960 }
961 "
962 };
963
964 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
965 let warnings = RefCell::new(Vec::new());
966
967 explore_crate(
968 &syn::parse_file(source).unwrap().items,
969 &PathBuf::new(),
970 &item_path("crate"),
971 |m| *m != module_skip,
972 &mut symbols_type,
973 |msg| warnings.borrow_mut().push(msg.to_owned()),
974 );
975
976 let expected: HashMap<ItemPath, SymbolType> = [
977 (item_path("crate"), SymbolType::Crate),
978 (item_path("crate::AStruct"), SymbolType::Struct),
979 (item_path("crate::skip"), SymbolType::Mod),
980 (item_path("crate::a"), SymbolType::Mod),
981 (item_path("crate::a::b"), SymbolType::Mod),
982 (item_path("crate::a::b::ATrait"), SymbolType::Trait),
983 (item_path("crate::a::FooStruct"), SymbolType::Struct),
984 ]
985 .into_iter()
986 .collect();
987
988 assert_eq!(symbols_type, expected);
989 }
990
991 #[test]
992 fn test_symbols_type_with_mod_under_cfg_test() {
993 let source = indoc! { "
994 #[cfg(not(test))]
995 mod a {
996 struct MyStruct {}
997 }
998
999 #[cfg(test)]
1000 mod a {
1001 struct MyStructTest {}
1002 }
1003
1004 #[cfg(test)]
1005 mod b {
1006 struct MyStructTest {}
1007 }
1008
1009 #[cfg(not(test))]
1010 mod b {
1011 struct MyStruct {}
1012 }
1013 "
1014 };
1015
1016 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1017 let warnings = RefCell::new(Vec::new());
1018
1019 explore_crate(
1020 &syn::parse_file(source).unwrap().items,
1021 &PathBuf::new(),
1022 &item_path("crate"),
1023 |_| true,
1024 &mut symbols_type,
1025 |msg| warnings.borrow_mut().push(msg.to_owned()),
1026 );
1027
1028 let expected: HashMap<ItemPath, SymbolType> = [
1029 (item_path("crate"), SymbolType::Crate),
1030 (item_path("crate::a"), SymbolType::Mod),
1031 (item_path("crate::a::MyStruct"), SymbolType::Struct),
1032 (item_path("crate::b"), SymbolType::Mod),
1033 (item_path("crate::b::MyStruct"), SymbolType::Struct),
1034 ]
1035 .into_iter()
1036 .collect();
1037
1038 assert_eq!(symbols_type, expected);
1039 }
1040
1041 #[test]
1042 fn test_symbols_type_multiple_module_first_wins() {
1043 let source = indoc! { "
1044 #[cfg(not(foo))]
1045 mod a {
1046 struct MyStruct {}
1047 }
1048
1049 #[cfg(foo)]
1050 mod a {
1051 struct Skip {}
1052 }
1053 "
1054 };
1055
1056 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1057 let warnings = RefCell::new(Vec::new());
1058
1059 explore_crate(
1060 &syn::parse_file(source).unwrap().items,
1061 &PathBuf::new(),
1062 &item_path("crate"),
1063 |_| true,
1064 &mut symbols_type,
1065 |msg| warnings.borrow_mut().push(msg.to_owned()),
1066 );
1067
1068 let expected: HashMap<ItemPath, SymbolType> = [
1069 (item_path("crate"), SymbolType::Crate),
1070 (item_path("crate::a"), SymbolType::Mod),
1071 (item_path("crate::a::MyStruct"), SymbolType::Struct),
1072 ]
1073 .into_iter()
1074 .collect();
1075
1076 assert_eq!(symbols_type, expected);
1077 }
1078
1079 #[test]
1080 fn test_traverse_module_expore_lazily() {
1081 let symbols: HashSet<ItemPath> = [item_path("crate::module")].into_iter().collect();
1082 let modules = all_ancestor_paths(symbols.iter());
1083
1084 let source = indoc! { "
1085 mod module {
1086 struct Foo {}
1087 }
1088 "
1089 };
1090
1091 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1092 let warnings = RefCell::new(Vec::new());
1093
1094 explore_crate(
1095 &syn::parse_file(source).unwrap().items,
1096 &PathBuf::new(),
1097 &item_path("crate"),
1098 |module| modules.contains(module),
1099 &mut symbols_type,
1100 |msg| warnings.borrow_mut().push(msg.to_owned()),
1101 );
1102
1103 let symbols_type: HashSet<ItemPath> = symbols_type.keys().cloned().collect();
1104
1105 let expected: HashSet<ItemPath> =
1107 [item_path("crate"), item_path("crate::module")].into_iter().collect();
1108
1109 assert_eq!(symbols_type, expected);
1110 }
1111
1112 #[test]
1113 fn test_documentation_url() {
1114 let config = IntralinksDocsRsConfig::default();
1115
1116 let symbols_type: HashMap<ItemPath, SymbolType> =
1117 [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1118
1119 let link = documentation_url(&item_path("crate"), &symbols_type, "foobini", None, &config);
1120 assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/"));
1121
1122 let symbols_type: HashMap<ItemPath, SymbolType> =
1123 [(item_path("crate::AStruct"), SymbolType::Struct)].into_iter().collect();
1124
1125 let link = documentation_url(
1126 &item_path("crate::AStruct"),
1127 &symbols_type,
1128 "foobini",
1129 None,
1130 &config,
1131 );
1132 assert_eq!(
1133 link.as_deref(),
1134 Some("https://docs.rs/foobini/latest/foobini/struct.AStruct.html")
1135 );
1136
1137 let symbols_type: HashMap<ItemPath, SymbolType> =
1138 [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1139
1140 let link = documentation_url(
1141 &item_path("crate::amodule"),
1142 &symbols_type,
1143 "foobini",
1144 None,
1145 &config,
1146 );
1147 assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/amodule/"));
1148
1149 let symbols_type: HashMap<ItemPath, SymbolType> =
1150 [(item_path("::std"), SymbolType::Crate)].into_iter().collect();
1151
1152 let link = documentation_url(&item_path("::std"), &symbols_type, "foobini", None, &config);
1153 assert_eq!(link.as_deref(), Some("https://doc.rust-lang.org/stable/std/"));
1154
1155 let symbols_type: HashMap<ItemPath, SymbolType> =
1156 [(item_path("::std::collections::HashMap"), SymbolType::Struct)].into_iter().collect();
1157
1158 let link = documentation_url(
1159 &item_path("::std::collections::HashMap"),
1160 &symbols_type,
1161 "foobini",
1162 None,
1163 &config,
1164 );
1165 assert_eq!(
1166 link.as_deref(),
1167 Some("https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html")
1168 );
1169
1170 let symbols_type: HashMap<ItemPath, SymbolType> =
1171 [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1172
1173 let link = documentation_url(
1174 &ItemPath::from_string("crate::amodule").unwrap(),
1175 &symbols_type,
1176 "foo-bar-mumble",
1177 None,
1178 &config,
1179 );
1180 assert_eq!(
1181 link.as_deref(),
1182 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amodule/")
1183 );
1184
1185 let symbols_type: HashMap<ItemPath, SymbolType> =
1186 [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1187
1188 let link = documentation_url(
1189 &ItemPath::from_string("crate").unwrap(),
1190 &symbols_type,
1191 "foo-bar-mumble",
1192 Some("#enums"),
1193 &config,
1194 );
1195 assert_eq!(
1196 link.as_deref(),
1197 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/#enums")
1198 );
1199
1200 let symbols_type: HashMap<ItemPath, SymbolType> =
1201 [(item_path("crate::amod"), SymbolType::Mod)].into_iter().collect();
1202
1203 let link = documentation_url(
1204 &ItemPath::from_string("crate::amod").unwrap(),
1205 &symbols_type,
1206 "foo-bar-mumble",
1207 Some("#structs"),
1208 &config,
1209 );
1210 assert_eq!(
1211 link.as_deref(),
1212 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amod/#structs")
1213 );
1214
1215 let symbols_type: HashMap<ItemPath, SymbolType> =
1216 [(item_path("crate::MyStruct"), SymbolType::Struct)].into_iter().collect();
1217
1218 let link = documentation_url(
1219 &ItemPath::from_string("crate::MyStruct").unwrap(),
1220 &symbols_type,
1221 "foo-bar-mumble",
1222 Some("#implementations"),
1223 &config,
1224 );
1225 assert_eq!(
1226 link.as_deref(),
1227 Some(
1228 "https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/struct.MyStruct.html#implementations"
1229 )
1230 );
1231
1232 let symbols_type: HashMap<ItemPath, SymbolType> = [
1233 (item_path("crate::mymod::MyStruct"), SymbolType::Struct),
1234 (
1235 item_path("crate::mymod::MyStruct::a_method"),
1236 SymbolType::ImplItem(ImplSymbolType::Method),
1237 ),
1238 ]
1239 .into_iter()
1240 .collect();
1241
1242 let link = documentation_url(
1243 &ItemPath::from_string("crate::mymod::MyStruct::a_method").unwrap(),
1244 &symbols_type,
1245 "foo-bar-mumble",
1246 Some("#thiswillbedropped"),
1247 &config,
1248 );
1249 assert_eq!(
1250 link.as_deref(),
1251 Some(
1252 "https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/mymod/struct.MyStruct.html#method.a_method"
1253 )
1254 );
1255
1256 let config = IntralinksDocsRsConfig {
1257 docs_rs_base_url: Some("https://docs.company.rs".to_owned()),
1258 docs_rs_version: Some("1.0.0".to_owned()),
1259 };
1260
1261 let symbols_type: HashMap<ItemPath, SymbolType> =
1262 [(item_path("crate::Foo"), SymbolType::Struct)].into_iter().collect();
1263
1264 let link =
1265 documentation_url(&item_path("crate::Foo"), &symbols_type, "foobini", None, &config);
1266 assert_eq!(
1267 link.as_deref(),
1268 Some("https://docs.company.rs/foobini/1.0.0/foobini/struct.Foo.html")
1269 );
1270 }
1271
1272 #[test]
1273 fn test_extract_markdown_intralink_symbols() {
1274 let doc = indoc! { "
1275 # Foobini
1276
1277 This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
1278 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1279
1280 Go ahead and check all the [structs in foo](crate::foo#structs).
1281 Also check [this](::std::sync::Arc) and [this](::alloc::sync::Arc).
1282
1283 We also support [reference][style] [links].
1284
1285 [style]: crate::amodule
1286 [links]: crate::foo#structs
1287 "
1288 };
1289
1290 let symbols = extract_markdown_intralink_symbols(&Doc::from_str(doc));
1291
1292 let expected: HashSet<ItemPath> = [
1293 item_path("crate"),
1294 item_path("crate::amodule"),
1295 item_path("crate::foo"),
1296 item_path("::std::sync::Arc"),
1297 item_path("::alloc::sync::Arc"),
1298 ]
1299 .into_iter()
1300 .collect();
1301
1302 assert_eq!(symbols, expected);
1303 }
1304
1305 #[test]
1306 fn test_rewrite_markdown_links() {
1307 let doc = indoc! { r"
1308 # Foobini
1309
1310 This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
1311 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1312
1313 This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
1314 should [wor\\k \[fi\]ne](f\\i\(n\)e).
1315
1316 Go ahead and check all the [structs in foo](crate::foo#structs) specifically
1317 [this one](crate::foo::BestStruct). Also, this is a nice function: [copy](::std::fs::copy).
1318
1319 [](crate::foo::BestStruct)
1320 "
1321 };
1322
1323 let symbols_type: HashMap<ItemPath, SymbolType> = [
1324 (item_path("crate"), SymbolType::Crate),
1325 (item_path("crate::amodule"), SymbolType::Mod),
1326 (item_path("crate::foo"), SymbolType::Mod),
1327 (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1328 (item_path("::std::fs::copy"), SymbolType::Fn),
1329 ]
1330 .into_iter()
1331 .collect();
1332
1333 let new_readme = rewrite_markdown_links(
1334 &Doc::from_str(doc),
1335 &symbols_type,
1336 "foobini",
1337 &|_| (),
1338 &IntralinksConfig::default(),
1339 &HashSet::new(),
1340 );
1341 let expected = indoc! { r"
1342 # Foobini
1343
1344 This [beautiful crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/)
1345 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1346
1347 This link is broken and this is not supported, but this
1348 should [wor\\k \[fi\]ne](f\\i\(n\)e).
1349
1350 Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically
1351 [this one](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html). Also, this is a nice function: [copy](https://doc.rust-lang.org/stable/std/fs/fn.copy.html).
1352
1353 [](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
1354 "
1355 };
1356
1357 assert_eq!(new_readme.as_string(), expected);
1358 }
1359
1360 #[test]
1361 fn test_rewrite_markdown_links_strip_links() {
1362 let doc = indoc! { r"
1363 # Foobini
1364
1365 This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
1366 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1367
1368 This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
1369 should [wor\\k \[fi\]ne](f\\i\(n\)e).
1370
1371 Go ahead and check all the [structs in foo](crate::foo#structs) specifically
1372 [this one](crate::foo::BestStruct). Also, this is a nice function: [copy](::std::fs::copy).
1373
1374 [](crate::foo::BestStruct)
1375
1376 It works with backtricks as well: [modules](`crate::amodule`). And with
1377 [reference-style links][ref] (preserving other [references][other]).
1378
1379 [ref]: crate::foo::AnotherStruct
1380 [other]: https://en.wikipedia.org/wiki/Reference_(computer_science)
1381 "
1382 };
1383
1384 let symbols_type: HashMap<ItemPath, SymbolType> = [
1385 (item_path("crate"), SymbolType::Crate),
1386 (item_path("crate::amodule"), SymbolType::Mod),
1387 (item_path("crate::foo"), SymbolType::Mod),
1388 (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1389 (item_path("crate::foo::AnotherStruct"), SymbolType::Struct),
1390 ]
1391 .into_iter()
1392 .collect();
1393
1394 let new_readme = rewrite_links(
1395 &Doc::from_str(doc),
1396 &symbols_type,
1397 "foobini",
1398 &|_| (),
1399 &IntralinksConfig { strip_links: Some(true), ..Default::default() },
1400 );
1401 let expected = indoc! { r"
1402 # Foobini
1403
1404 This beautiful crate is cool because it contains modules
1405 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1406
1407 This link is broken and this is not supported, but this
1408 should [wor\\k \[fi\]ne](f\\i\(n\)e).
1409
1410 Go ahead and check all the structs in foo specifically
1411 this one. Also, this is a nice function: copy.
1412
1413 
1414
1415 It works with backtricks as well: modules. And with
1416 reference-style links (preserving other [references][other]).
1417
1418 [other]: https://en.wikipedia.org/wiki/Reference_(computer_science)
1419 "
1420 };
1421
1422 assert_eq!(new_readme.as_string(), expected);
1423 }
1424
1425 #[test]
1426 fn test_rewrite_markdown_links_backticked() {
1427 let doc = indoc! { r"
1428 # Foobini
1429
1430 This [beautiful crate](`crate`) is cool because it contains [modules](`crate::amodule`)
1431 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1432
1433 This link is [broken](`crate::broken`) and this is [not supported](`::foo::bar`), but this
1434 should [wor\\k \[fi\]ne](f\\i\(n\)e).
1435
1436 Go ahead and check all the [structs in foo](`crate::foo#structs`) and
1437 [structs in foo](`crate::foo`#structs) specifically [this one](`crate::foo::BestStruct`).
1438 Also, this is a nice function: [copy](`::std::fs::copy`).
1439
1440 [](`crate::foo::BestStruct`)
1441 "
1442 };
1443
1444 let symbols_type: HashMap<ItemPath, SymbolType> = [
1445 (item_path("crate"), SymbolType::Crate),
1446 (item_path("crate::amodule"), SymbolType::Mod),
1447 (item_path("crate::foo"), SymbolType::Mod),
1448 (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1449 (item_path("::std::fs::copy"), SymbolType::Fn),
1450 ]
1451 .into_iter()
1452 .collect();
1453
1454 let new_readme = rewrite_markdown_links(
1455 &Doc::from_str(doc),
1456 &symbols_type,
1457 "foobini",
1458 &|_| (),
1459 &IntralinksConfig::default(),
1460 &HashSet::new(),
1461 );
1462 let expected = indoc! { r"
1463 # Foobini
1464
1465 This [beautiful crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/)
1466 and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1467
1468 This link is broken and this is not supported, but this
1469 should [wor\\k \[fi\]ne](f\\i\(n\)e).
1470
1471 Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) and
1472 [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically [this one](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html).
1473 Also, this is a nice function: [copy](https://doc.rust-lang.org/stable/std/fs/fn.copy.html).
1474
1475 [](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
1476 "
1477 };
1478
1479 assert_eq!(new_readme.as_string(), expected);
1480 }
1481
1482 #[test]
1483 fn test_markdown_reference_definitions() {
1484 let doc = indoc! { r#"
1485 # Foobini
1486
1487 This [beautiful crate] is cool because it contains [modules]
1488 and some other [stuff] as well.
1489
1490 This link is [broken] and this is [not supported],
1491 but this should [wor\\k \[fi\]ne].
1492
1493 Go ahead and check all the [structs in foo] specifically
1494 [this one]. Also, this is a nice function: [copy][cp].
1495
1496 [![BestStruct doc]][BestStruct]
1497
1498 [beautiful crate]: crate
1499 [modules]: crate::amodule
1500 [stuff]: https://en.wikipedia.org/wiki/Stuff
1501 [broken]: crate::broken
1502 [not supported]: ::foo::bar
1503 [wor\\k \[fi\]ne]: f\\i\(n\)e
1504 [structs in foo]: crate::foo#structs
1505 [this one]: crate::foo::BestStruct
1506 [cp]: ::std::fs::copy#examples "A title here"
1507 [BestStruct doc]: https://example.com/image.png
1508 [BestStruct]: crate::foo::BestStruct
1509 "#
1510 };
1511
1512 let symbols_type: HashMap<ItemPath, SymbolType> = [
1513 (item_path("crate"), SymbolType::Crate),
1514 (item_path("crate::amodule"), SymbolType::Mod),
1515 (item_path("crate::foo"), SymbolType::Mod),
1516 (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1517 (item_path("::std::fs::copy"), SymbolType::Fn),
1518 ]
1519 .into_iter()
1520 .collect();
1521
1522 let new_readme = rewrite_links(
1523 &Doc::from_str(doc),
1524 &symbols_type,
1525 "foobini",
1526 &|_| (),
1527 &IntralinksConfig::default(),
1528 );
1529 let expected = indoc! { r#"
1530 # Foobini
1531
1532 This [beautiful crate] is cool because it contains [modules]
1533 and some other [stuff] as well.
1534
1535 This link is broken and this is not supported,
1536 but this should [wor\\k \[fi\]ne].
1537
1538 Go ahead and check all the [structs in foo] specifically
1539 [this one]. Also, this is a nice function: [copy][cp].
1540
1541 [![BestStruct doc]][BestStruct]
1542
1543 [beautiful crate]: https://docs.rs/foobini/latest/foobini/
1544 [modules]: https://docs.rs/foobini/latest/foobini/amodule/
1545 [stuff]: https://en.wikipedia.org/wiki/Stuff
1546 [wor\\k \[fi\]ne]: f\\i\(n\)e
1547 [structs in foo]: https://docs.rs/foobini/latest/foobini/foo/#structs
1548 [this one]: https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html
1549 [cp]: https://doc.rust-lang.org/stable/std/fs/fn.copy.html#examples "A title here"
1550 [BestStruct doc]: https://example.com/image.png
1551 [BestStruct]: https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html
1552 "#
1553 };
1554
1555 assert_eq!(new_readme.as_string(), expected);
1556 }
1557
1558 #[test]
1559 fn test_rewrite_markdown_links_removes_links() {
1560 let doc = indoc! { r"
1561 # Foobini
1562
1563 This crate has multiple [modules][mod a]. This link is [broken] and [so is this][null].
1564
1565 [mod a]: crate::amodule
1566 [broken]: crate::broken
1567 [null]: crate::nothing
1568 "
1569 };
1570
1571 let symbols_type: HashMap<ItemPath, SymbolType> =
1572 [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1573
1574 let new_readme = rewrite_links(
1575 &Doc::from_str(doc),
1576 &symbols_type,
1577 "foobini",
1578 &|_| (),
1579 &IntralinksConfig::default(),
1580 );
1581 let expected = indoc! { r"
1582 # Foobini
1583
1584 This crate has multiple [modules][mod a]. This link is broken and so is this.
1585
1586 [mod a]: https://docs.rs/foobini/latest/foobini/amodule/
1587 "
1588 };
1589
1590 assert_eq!(new_readme.as_string(), expected);
1591 }
1592
1593 #[test]
1594 fn test_markdown_impl_items() {
1595 let doc = indoc! { r#"
1596 # Foobini
1597
1598 This crate has [`Foo::new()`](`crate::Foo::new`), [`Foo::a_method()`](`crate::Foo::a_method`),
1599 and [`Foo::another_method()`](`crate::Foo::another_method`).
1600
1601 It also has [`Foo::no_self()`](`crate::Foo::no_self`). There's also [`Bar::beer()`](`crate::amod::Bar::beer`).
1602
1603 Struct `Foo` has a [type called `baz`](`crate::Foo::Baz`) and a
1604 [const called `number`](`crate::Foo::number`).
1605
1606 We have a function in `FooAlias` [called `hello`](`crate::FooAlias::hello`).
1607
1608 And in `MyEnum` we have [called `hey`](`crate::MyEnum::hey`).
1609
1610 And in `MyUnion` we have [called `sup`](`crate::MyUnion::sup`).
1611 "#
1612 };
1613
1614 let symbols_type: HashMap<ItemPath, SymbolType> = [
1615 (item_path("crate"), SymbolType::Crate),
1616 (item_path("crate::Foo"), SymbolType::Struct),
1617 (item_path("crate::Foo::new"), SymbolType::ImplItem(ImplSymbolType::Method)),
1618 (item_path("crate::Foo::a_method"), SymbolType::ImplItem(ImplSymbolType::Method)),
1619 (item_path("crate::Foo::another_method"), SymbolType::ImplItem(ImplSymbolType::Method)),
1620 (item_path("crate::Foo::no_self"), SymbolType::ImplItem(ImplSymbolType::Method)),
1621 (item_path("crate::amod::Bar"), SymbolType::Struct),
1622 (item_path("crate::amod::Bar::beer"), SymbolType::ImplItem(ImplSymbolType::Method)),
1623 (item_path("crate::Foo::Baz"), SymbolType::ImplItem(ImplSymbolType::Type)),
1624 (item_path("crate::Foo::number"), SymbolType::ImplItem(ImplSymbolType::Const)),
1625 (item_path("crate::FooAlias"), SymbolType::Type),
1626 (item_path("crate::FooAlias::hello"), SymbolType::ImplItem(ImplSymbolType::Method)),
1627 (item_path("crate::MyEnum"), SymbolType::Enum),
1628 (item_path("crate::MyEnum::hey"), SymbolType::ImplItem(ImplSymbolType::Method)),
1629 (item_path("crate::MyUnion"), SymbolType::Union),
1630 (item_path("crate::MyUnion::sup"), SymbolType::ImplItem(ImplSymbolType::Method)),
1631 ]
1632 .into_iter()
1633 .collect();
1634
1635 let new_readme = rewrite_links(
1636 &Doc::from_str(doc),
1637 &symbols_type,
1638 "foobini",
1639 &|_| (),
1640 &IntralinksConfig::default(),
1641 );
1642 let expected = indoc! { r#"
1643 # Foobini
1644
1645 This crate has [`Foo::new()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.new), [`Foo::a_method()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.a_method),
1646 and [`Foo::another_method()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.another_method).
1647
1648 It also has [`Foo::no_self()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.no_self). There's also [`Bar::beer()`](https://docs.rs/foobini/latest/foobini/amod/struct.Bar.html#method.beer).
1649
1650 Struct `Foo` has a [type called `baz`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#associatedtype.Baz) and a
1651 [const called `number`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#associatedconstant.number).
1652
1653 We have a function in `FooAlias` [called `hello`](https://docs.rs/foobini/latest/foobini/type.FooAlias.html#method.hello).
1654
1655 And in `MyEnum` we have [called `hey`](https://docs.rs/foobini/latest/foobini/enum.MyEnum.html#method.hey).
1656
1657 And in `MyUnion` we have [called `sup`](https://docs.rs/foobini/latest/foobini/union.MyUnion.html#method.sup).
1658 "#
1659 };
1660
1661 assert_eq!(new_readme.as_string(), expected);
1662 }
1663}