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