1use crate::transform::intralinks::links::{
7 markdown_link_iterator, markdown_reference_link_definition_iterator, Link, MarkdownLink,
8};
9use crate::transform::DocTransform;
10use crate::Doc;
11use module_walker::walk_module_file;
12use std::collections::{HashMap, HashSet};
13use std::fmt;
14use std::hash::{Hash, Hasher};
15use std::path::{Path, PathBuf};
16use std::rc::Rc;
17use syn::{Item, ItemMod};
18use thiserror::Error;
19use unicase::UniCase;
20
21mod links;
22mod module_walker;
23
24#[derive(Error, Debug)]
25pub enum IntralinkError {
26 #[error("IO error: {0}")]
27 IOError(std::io::Error),
28 #[error("failed to analyzing code: {0}")]
29 AstWalkError(module_walker::ModuleWalkError),
30 #[error("failed to load standard library: {0}")]
31 LoadStdLibError(String),
32}
33
34impl From<std::io::Error> for IntralinkError {
35 fn from(err: std::io::Error) -> Self {
36 IntralinkError::IOError(err)
37 }
38}
39
40impl From<module_walker::ModuleWalkError> for IntralinkError {
41 fn from(err: module_walker::ModuleWalkError) -> Self {
42 IntralinkError::AstWalkError(err)
43 }
44}
45
46#[derive(Default, Debug, PartialEq, Eq, Clone)]
47pub struct IntralinksDocsRsConfig {
48 pub docs_rs_base_url: Option<String>,
49 pub docs_rs_version: Option<String>,
50}
51
52#[derive(Default, Debug, PartialEq, Eq, Clone)]
53pub struct IntralinksConfig {
54 pub docs_rs: IntralinksDocsRsConfig,
55 pub strip_links: Option<bool>,
56}
57
58pub struct DocTransformIntralinks<F> {
59 crate_name: String,
60 entrypoint: PathBuf,
61 emit_warning: F,
62 config: IntralinksConfig,
63}
64
65impl<F> DocTransformIntralinks<F>
66where
67 F: Fn(&str),
68{
69 pub fn new(
70 crate_name: impl Into<String>,
71 entrypoint: impl AsRef<Path>,
72 emit_warning: F,
73 config: Option<IntralinksConfig>,
74 ) -> DocTransformIntralinks<F> {
75 DocTransformIntralinks {
76 crate_name: crate_name.into(),
77 entrypoint: entrypoint.as_ref().to_path_buf(),
78 emit_warning,
79 config: config.unwrap_or_default(),
80 }
81 }
82}
83
84impl<F> DocTransform for DocTransformIntralinks<F>
85where
86 F: Fn(&str),
87{
88 type E = IntralinkError;
89
90 fn transform(&self, doc: &Doc) -> Result<Doc, IntralinkError> {
91 let symbols: HashSet<ItemPath> = extract_markdown_intralink_symbols(doc);
92
93 if symbols.is_empty() {
95 return Ok(doc.clone());
96 }
97
98 let symbols_type = match self.config.strip_links.unwrap_or(false) {
100 false => load_symbols_type(&self.entrypoint, &symbols, &self.emit_warning)?,
101 true => HashMap::new(),
102 };
103
104 let doc =
105 rewrite_links(doc, &symbols_type, &self.crate_name, &self.emit_warning, &self.config);
106
107 Ok(doc)
108 }
109}
110
111fn rewrite_links(
112 doc: &Doc,
113 symbols_type: &HashMap<ItemPath, SymbolType>,
114 crate_name: &str,
115 emit_warning: &impl Fn(&str),
116 config: &IntralinksConfig,
117) -> Doc {
118 let RewriteReferenceLinksResult { doc, reference_links_to_remove } =
119 rewrite_reference_links_definitions(doc, symbols_type, crate_name, emit_warning, config);
120
121 rewrite_markdown_links(
124 &doc,
125 symbols_type,
126 crate_name,
127 emit_warning,
128 config,
129 &reference_links_to_remove,
130 )
131}
132
133#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
134pub enum ItemPathAnchor {
135 Root,
137 Crate,
139}
140
141#[derive(Clone)]
143pub struct ItemPath {
144 pub anchor: ItemPathAnchor,
145
146 path_shared: Rc<Vec<String>>,
149 path_end: usize,
150}
151
152impl ItemPath {
153 fn new(anchor: ItemPathAnchor) -> ItemPath {
154 ItemPath { anchor, path_shared: Rc::new(Vec::new()), path_end: 0 }
155 }
156
157 fn root(crate_name: &str) -> ItemPath {
158 ItemPath::new(ItemPathAnchor::Root).join(&crate_name)
159 }
160
161 fn from_string(s: &str) -> Option<ItemPath> {
162 let anchor;
163 let rest;
164
165 if let Some(r) = s.strip_prefix("::") {
166 anchor = ItemPathAnchor::Root;
167 rest = r;
168 } else if s == "crate" {
169 return Some(ItemPath::new(ItemPathAnchor::Crate));
170 } else if let Some(r) = s.strip_prefix("crate::") {
171 anchor = ItemPathAnchor::Crate;
172 rest = r;
173 } else {
174 return None;
175 }
176
177 if rest.is_empty() {
178 return None;
179 }
180
181 let path: Rc<Vec<String>> = Rc::new(rest.split("::").map(str::to_owned).collect());
182
183 Some(ItemPath { anchor, path_end: path.len(), path_shared: path })
184 }
185
186 fn path_components(&self) -> impl Iterator<Item = &str> {
187 self.path_shared[0..self.path_end].iter().map(String::as_str)
188 }
189
190 fn is_toplevel(&self) -> bool {
191 match self.anchor {
192 ItemPathAnchor::Root => self.path_end <= 1,
193 ItemPathAnchor::Crate => self.path_end == 0,
194 }
195 }
196
197 fn parent(mut self) -> Option<ItemPath> {
198 match self.is_toplevel() {
199 true => None,
200 false => {
201 self.path_end -= 1;
202 Some(self)
203 }
204 }
205 }
206
207 fn name(&self) -> Option<&str> {
208 self.path_end.checked_sub(1).and_then(|i| self.path_shared.get(i)).map(String::as_str)
209 }
210
211 fn join(mut self, s: &impl ToString) -> ItemPath {
212 let path = Rc::make_mut(&mut self.path_shared);
213 path.truncate(self.path_end);
214 path.push(s.to_string());
215 self.path_end += 1;
216 self
217 }
218
219 fn all_ancestors(&self) -> impl Iterator<Item = ItemPath> {
220 let first_ancestor = self.clone().parent();
221
222 std::iter::successors(first_ancestor, |ancestor| ancestor.clone().parent())
223 }
224}
225
226impl PartialEq for ItemPath {
227 fn eq(&self, other: &Self) -> bool {
228 self.anchor == other.anchor && self.path_components().eq(other.path_components())
229 }
230}
231
232impl Eq for ItemPath {}
233
234impl Hash for ItemPath {
235 fn hash<H: Hasher>(&self, state: &mut H) {
236 self.anchor.hash(state);
237 self.path_components().for_each(|c| c.hash(state));
238 }
239}
240
241impl fmt::Display for ItemPath {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
243 match self.anchor {
244 ItemPathAnchor::Root => (),
245 ItemPathAnchor::Crate => f.write_str("crate")?,
246 }
247
248 for s in self.path_components() {
249 f.write_str("::")?;
250 f.write_str(s)?;
251 }
252
253 Ok(())
254 }
255}
256
257impl fmt::Debug for ItemPath {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
259 fmt::Display::fmt(self, f)
260 }
261}
262
263#[derive(Copy, Clone, Debug, PartialEq, Eq)]
264pub enum ImplSymbolType {
265 Method,
266 Const,
267 Type,
268}
269
270#[derive(Copy, Clone, Debug, PartialEq, Eq)]
271pub enum SymbolType {
272 Crate,
273 Struct,
274 Trait,
275 Enum,
276 Union,
277 Type,
278 Mod,
279 Macro,
280 Const,
281 Fn,
282 Static,
283 ImplItem(ImplSymbolType),
284}
285
286impl SymbolType {
287 fn get_module_path(self, path: &ItemPath) -> Option<ItemPath> {
292 match self {
293 SymbolType::Crate => {
294 assert!(path.is_toplevel(), "a crate should always be in a toplevel path");
295 None
296 }
297 SymbolType::Struct
298 | SymbolType::Trait
299 | SymbolType::Enum
300 | SymbolType::Union
301 | SymbolType::Type
302 | SymbolType::Mod
303 | SymbolType::Macro
304 | SymbolType::Const
305 | SymbolType::Fn
306 | SymbolType::Static => {
307 let p = path.clone().parent().unwrap_or_else(|| {
308 panic!("item {path} of type {self:?} should have a parent module")
309 });
310 Some(p)
311 }
312 SymbolType::ImplItem(_) => {
313 let p = path
314 .clone()
315 .parent()
316 .unwrap_or_else(|| {
317 panic!("item {path} of type {self:?} should have a parent type")
318 })
319 .parent()
320 .unwrap_or_else(|| {
321 panic!("item {path} of type {self:?} should have a parent module")
322 });
323 Some(p)
324 }
325 }
326 }
327}
328
329fn symbols_type_impl_block(
330 module: &ItemPath,
331 impl_block: &syn::ItemImpl,
332) -> Vec<(ItemPath, SymbolType)> {
333 use syn::{ImplItem, Type, TypePath};
334
335 if let Type::Path(TypePath { qself: None, path }) = &*impl_block.self_ty {
336 if let Some(self_ident) = path.get_ident().map(ToString::to_string) {
337 let self_path = module.clone().join(&self_ident);
338
339 return impl_block
340 .items
341 .iter()
342 .filter_map(|item| match item {
343 ImplItem::Fn(m) => {
344 let ident = m.sig.ident.to_string();
345
346 Some((ident, ImplSymbolType::Method))
347 }
348 ImplItem::Const(c) => {
349 let ident = c.ident.to_string();
350
351 Some((ident, ImplSymbolType::Const))
352 }
353 ImplItem::Type(t) => {
354 let ident = t.ident.to_string();
355
356 Some((ident, ImplSymbolType::Type))
357 }
358 _ => None,
359 })
360 .map(|(ident, tpy)| (self_path.clone().join(&ident), SymbolType::ImplItem(tpy)))
361 .collect();
362 }
363 }
364
365 Vec::new()
366}
367
368fn item_symbols_type(module: &ItemPath, item: &Item) -> Vec<(ItemPath, SymbolType)> {
369 let item_path = |ident: &syn::Ident| module.clone().join(ident);
370
371 let (path, symbol_type) = match item {
372 Item::Enum(e) => (item_path(&e.ident), SymbolType::Enum),
373 Item::Struct(s) => (item_path(&s.ident), SymbolType::Struct),
374 Item::Trait(t) => (item_path(&t.ident), SymbolType::Trait),
375 Item::Union(u) => (item_path(&u.ident), SymbolType::Union),
376 Item::Type(t) => (item_path(&t.ident), SymbolType::Type),
377 Item::Mod(m) => (item_path(&m.ident), SymbolType::Mod),
378 Item::Macro(syn::ItemMacro { ident: Some(ident), .. }) => {
379 (item_path(ident), SymbolType::Macro)
380 }
381 Item::Const(c) => (item_path(&c.ident), SymbolType::Const),
382 Item::Fn(f) => (item_path(&f.sig.ident), SymbolType::Fn),
383 Item::Static(s) => (item_path(&s.ident), SymbolType::Static),
384 Item::Impl(impl_block) => {
385 return symbols_type_impl_block(module, impl_block);
386 }
387
388 _ => return Vec::new(),
389 };
390
391 vec![(path, symbol_type)]
392}
393
394fn is_cfg_test(attribute: &syn::Attribute) -> bool {
395 let test_attribute: syn::Attribute = syn::parse_quote!(#[cfg(test)]);
396
397 *attribute == test_attribute
398}
399
400fn visit_module_item(
401 save_symbol: impl Fn(&ItemPath) -> bool,
402 symbols_type: &mut HashMap<ItemPath, SymbolType>,
403 module: &ItemPath,
404 item: &Item,
405) {
406 for (symbol, symbol_type) in item_symbols_type(module, item) {
407 if save_symbol(&symbol) {
408 symbols_type.insert(symbol, symbol_type);
409 }
410 }
411}
412
413fn check_explore_module(
415 should_explore_module: impl Fn(&ItemPath) -> bool,
416 modules_visited: &mut HashSet<ItemPath>,
417 mod_symbol: &ItemPath,
418 mod_item: &ItemMod,
419) -> bool {
420 if modules_visited.contains(mod_symbol) {
431 return false;
432 }
433
434 if mod_item.attrs.iter().any(is_cfg_test) {
437 return false;
438 }
439
440 let explore = should_explore_module(mod_symbol);
441
442 if explore {
443 modules_visited.insert(mod_symbol.clone());
444 }
445
446 explore
447}
448
449fn explore_crate<P: AsRef<Path>>(
450 file: P,
451 crate_symbol: &ItemPath,
452 symbols: &HashSet<ItemPath>,
453 paths_to_explore: &HashSet<ItemPath>,
454 symbols_type: &mut HashMap<ItemPath, SymbolType>,
455 emit_warning: &impl Fn(&str),
456) -> Result<(), module_walker::ModuleWalkError> {
457 let mut modules_visited: HashSet<ItemPath> = HashSet::new();
458
459 symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
461
462 let mut visit = |module: &ItemPath, item: &Item| {
463 let save_symbol = |symbol: &ItemPath| {
464 symbols.contains(symbol) || paths_to_explore.contains(symbol)
468 };
469
470 visit_module_item(save_symbol, symbols_type, module, item);
471 };
472
473 let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
474 check_explore_module(
475 |mod_symbol| paths_to_explore.contains(mod_symbol),
476 &mut modules_visited,
477 mod_symbol,
478 mod_item,
479 )
480 };
481
482 walk_module_file(file, crate_symbol, &mut visit, &mut explore_module, emit_warning)
483}
484
485fn load_symbols_type<P: AsRef<Path>>(
486 entry_point: P,
487 symbols: &HashSet<ItemPath>,
488 emit_warning: &impl Fn(&str),
489) -> Result<HashMap<ItemPath, SymbolType>, IntralinkError> {
490 let paths_to_explore: HashSet<ItemPath> = all_ancestor_paths(symbols.iter());
491 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
492
493 let std_lib_crates = match references_standard_library(symbols) {
495 true => get_standard_libraries()?,
496 false => Vec::new(),
497 };
498
499 for Crate { name, entrypoint } in std_lib_crates {
500 explore_crate(
501 entrypoint,
502 &ItemPath::root(&name),
503 symbols,
504 &paths_to_explore,
505 &mut symbols_type,
506 emit_warning,
507 )?;
508 }
509
510 explore_crate(
511 entry_point,
512 &ItemPath::new(ItemPathAnchor::Crate),
513 symbols,
514 &paths_to_explore,
515 &mut symbols_type,
516 emit_warning,
517 )?;
518
519 Ok(symbols_type)
520}
521
522fn all_ancestor_paths<'a>(symbols: impl Iterator<Item = &'a ItemPath>) -> HashSet<ItemPath> {
526 symbols.into_iter().flat_map(ItemPath::all_ancestors).collect()
527}
528
529fn extract_markdown_intralink_symbols(doc: &Doc) -> HashSet<ItemPath> {
530 let item_paths_inline_links =
531 markdown_link_iterator(&doc.markdown).items().filter_map(|l| match l {
532 MarkdownLink::Inline { link: inline_link } => inline_link.link.link_as_item_path(),
533 MarkdownLink::Reference { .. } => None,
534 });
535
536 let item_paths_reference_link_def = markdown_reference_link_definition_iterator(&doc.markdown)
537 .items()
538 .filter_map(|l| l.link.link_as_item_path());
539
540 item_paths_inline_links.chain(item_paths_reference_link_def).collect()
541}
542
543fn documentation_url(
547 item_path: &ItemPath,
548 symbols_type: &HashMap<ItemPath, SymbolType>,
549 crate_name: &str,
550 fragment: Option<&str>,
551 config: &IntralinksDocsRsConfig,
552) -> Option<String> {
553 let package_name = crate_name.replace('-', "_");
554 let typ = *symbols_type.get(item_path)?;
555
556 let mut link = match item_path.anchor {
557 ItemPathAnchor::Root => {
558 let std_crate_name =
559 item_path.path_components().next().expect("a root path should not be empty");
560 format!("https://doc.rust-lang.org/stable/{std_crate_name}/")
561 }
562 ItemPathAnchor::Crate => {
563 let base_url =
564 config.docs_rs_base_url.as_ref().map_or("https://docs.rs", String::as_str);
565 let version = config.docs_rs_version.as_ref().map_or("latest", String::as_str);
566
567 format!("{base_url}/{crate_name}/{version}/{package_name}/")
568 }
569 };
570
571 if typ == SymbolType::Crate {
572 return Some(format!("{}{}", link, fragment.unwrap_or("")));
573 }
574
575 let skip_components = match item_path.anchor {
576 ItemPathAnchor::Root => 1,
577 ItemPathAnchor::Crate => 0,
578 };
579
580 let module_path = typ.get_module_path(item_path).expect("item should belong to a module");
581
582 for s in module_path.path_components().skip(skip_components) {
583 link.push_str(s);
584 link.push('/');
585 }
586
587 let name =
588 item_path.name().unwrap_or_else(|| panic!("failed to get last component of {item_path}"));
589
590 match typ {
591 SymbolType::Crate => unreachable!(),
592 SymbolType::Struct => link.push_str(&format!("struct.{name}.html")),
593 SymbolType::Trait => link.push_str(&format!("trait.{name}.html")),
594 SymbolType::Enum => link.push_str(&format!("enum.{name}.html")),
595 SymbolType::Union => link.push_str(&format!("union.{name}.html")),
596 SymbolType::Type => link.push_str(&format!("type.{name}.html")),
597 SymbolType::Mod => link.push_str(&format!("{name}/")),
598 SymbolType::Macro => link.push_str(&format!("macro.{name}.html")),
599 SymbolType::Const => link.push_str(&format!("const.{name}.html")),
600 SymbolType::Fn => link.push_str(&format!("fn.{name}.html")),
601 SymbolType::Static => link.push_str(&format!("static.{name}.html")),
602 SymbolType::ImplItem(typ) => {
603 let parent_path = item_path
604 .clone()
605 .parent()
606 .unwrap_or_else(|| panic!("item {item_path} should always have a parent"));
607
608 let link = documentation_url(
609 &parent_path,
610 symbols_type,
611 crate_name,
612 None,
614 config,
615 )?;
616
617 let impl_item_fragment_str = match typ {
618 ImplSymbolType::Method => "method",
619 ImplSymbolType::Const => "associatedconstant",
620 ImplSymbolType::Type => "associatedtype",
621 };
622
623 return Some(format!("{link}#{impl_item_fragment_str}.{name}"));
624 }
625 }
626
627 Some(format!("{}{}", link, fragment.unwrap_or("")))
628}
629
630enum MarkdownLinkAction {
631 Link(Link),
632 Preserve,
633 Strip,
634}
635
636fn markdown_link(
637 link: &Link,
638 symbols_type: &HashMap<ItemPath, SymbolType>,
639 crate_name: &str,
640 emit_warning: &impl Fn(&str),
641 config: &IntralinksConfig,
642) -> MarkdownLinkAction {
643 match link.link_as_item_path() {
644 Some(symbol) => {
645 let link = documentation_url(
646 &symbol,
647 symbols_type,
648 crate_name,
649 link.link_fragment(),
650 &config.docs_rs,
651 );
652
653 match link {
654 Some(l) => MarkdownLinkAction::Link(l.into()),
655 None => {
656 emit_warning(&format!("Could not resolve definition of `{symbol}`."));
657
658 MarkdownLinkAction::Strip
660 }
661 }
662 }
663 None => MarkdownLinkAction::Preserve,
664 }
665}
666
667fn rewrite_markdown_links(
668 doc: &Doc,
669 symbols_type: &HashMap<ItemPath, SymbolType>,
670 crate_name: &str,
671 emit_warning: &impl Fn(&str),
672 config: &IntralinksConfig,
673 reference_links_to_remove: &HashSet<UniCase<String>>,
674) -> Doc {
675 use crate::utils::ItemOrOther;
676
677 let strip_links = config.strip_links.unwrap_or(false);
678 let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
679
680 for item_or_other in markdown_link_iterator(&doc.markdown).complete() {
681 match item_or_other {
682 ItemOrOther::Item(MarkdownLink::Inline { link: inline_link }) => {
683 let markdown_link: MarkdownLinkAction = match strip_links {
684 false => markdown_link(
685 &inline_link.link,
686 symbols_type,
687 crate_name,
688 emit_warning,
689 config,
690 ),
691 true => match inline_link.link.link_as_item_path() {
692 None => MarkdownLinkAction::Preserve,
693 Some(_) => MarkdownLinkAction::Strip,
694 },
695 };
696
697 match markdown_link {
698 MarkdownLinkAction::Link(markdown_link) => {
699 new_doc.push_str(&inline_link.with_link(markdown_link).to_string());
700 }
701 MarkdownLinkAction::Preserve => {
702 new_doc.push_str(&inline_link.to_string());
703 }
704 MarkdownLinkAction::Strip => {
705 new_doc.push_str(&inline_link.text);
706 }
707 }
708 }
709 ItemOrOther::Item(MarkdownLink::Reference { link }) => {
710 match reference_links_to_remove.contains(link.label()) {
711 true => new_doc.push_str(link.text()),
712 false => new_doc.push_str(&link.to_string()),
713 }
714 }
715 ItemOrOther::Other(other) => {
716 new_doc.push_str(other);
717 }
718 }
719 }
720
721 Doc::from_str(new_doc)
722}
723
724struct RewriteReferenceLinksResult {
725 doc: Doc,
726 reference_links_to_remove: HashSet<UniCase<String>>,
727}
728
729fn rewrite_reference_links_definitions(
730 doc: &Doc,
731 symbols_type: &HashMap<ItemPath, SymbolType>,
732 crate_name: &str,
733 emit_warning: &impl Fn(&str),
734 config: &IntralinksConfig,
735) -> RewriteReferenceLinksResult {
736 use crate::utils::ItemOrOther;
737 let mut reference_links_to_remove = HashSet::new();
738 let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
739 let mut skip_next_newline = false;
740 let strip_links = config.strip_links.unwrap_or(false);
741
742 let iter = markdown_reference_link_definition_iterator(&doc.markdown);
743
744 for item_or_other in iter.complete() {
745 match item_or_other {
746 ItemOrOther::Item(link_ref_def) => {
747 let markdown_link: MarkdownLinkAction = match strip_links {
748 false => markdown_link(
749 &link_ref_def.link,
750 symbols_type,
751 crate_name,
752 emit_warning,
753 config,
754 ),
755 true => match link_ref_def.link.link_as_item_path() {
756 None => MarkdownLinkAction::Preserve,
757 Some(_) => MarkdownLinkAction::Strip,
758 },
759 };
760
761 match markdown_link {
762 MarkdownLinkAction::Link(link) => {
763 new_doc.push_str(&link_ref_def.with_link(link).to_string());
764 }
765 MarkdownLinkAction::Preserve => {
766 new_doc.push_str(&link_ref_def.to_string());
767 }
768 MarkdownLinkAction::Strip => {
769 reference_links_to_remove.insert(link_ref_def.label);
771 skip_next_newline = true;
772 }
773 }
774 }
775 ItemOrOther::Other(other) => {
776 let other = match skip_next_newline {
777 true => {
778 skip_next_newline = false;
779 let next_index = other
780 .chars()
781 .enumerate()
782 .skip_while(|(_, c)| c.is_whitespace() && *c != '\n')
783 .skip(1)
784 .map(|(i, _)| i)
785 .next();
786
787 next_index.and_then(|i| other.get(i..)).unwrap_or("")
788 }
789 false => other,
790 };
791 new_doc.push_str(other);
792 }
793 }
794 }
795
796 RewriteReferenceLinksResult { doc: Doc::from_str(new_doc), reference_links_to_remove }
797}
798
799fn get_rustc_sysroot_libraries_dir() -> Result<PathBuf, IntralinkError> {
800 use std::process::Command;
801
802 let output = Command::new("rustc")
803 .args(["--print=sysroot"])
804 .output()
805 .map_err(|e| IntralinkError::LoadStdLibError(format!("failed to run rustc: {e}")))?;
806
807 let s = String::from_utf8(output.stdout).expect("unexpected output from rustc");
808 let sysroot = PathBuf::from(s.trim());
809 let src_path = sysroot.join("lib").join("rustlib").join("src").join("rust").join("library");
810
811 match src_path.is_dir() {
812 false => Err(IntralinkError::LoadStdLibError(format!(
813 "Cannot find rust standard library in \"{}\"",
814 src_path.display()
815 ))),
816 true => Ok(src_path),
817 }
818}
819
820#[derive(Debug)]
821struct Crate {
822 name: String,
823 entrypoint: PathBuf,
824}
825
826fn references_standard_library(symbols: &HashSet<ItemPath>) -> bool {
827 symbols.iter().any(|symbol| symbol.anchor == ItemPathAnchor::Root)
829}
830
831fn get_standard_libraries() -> Result<Vec<Crate>, IntralinkError> {
832 let libraries_dir = get_rustc_sysroot_libraries_dir()?;
833 let mut std_libs = Vec::with_capacity(64);
834
835 for entry in std::fs::read_dir(libraries_dir)? {
836 let entry = entry?;
837 let project_dir_path = entry.path();
838 let cargo_manifest_path = project_dir_path.join("Cargo.toml");
839 let lib_entrypoint = project_dir_path.join("src").join("lib.rs");
840
841 if cargo_manifest_path.is_file() && lib_entrypoint.is_file() {
842 let crate_name =
843 crate::project_package_name(&cargo_manifest_path).ok_or_else(|| {
844 IntralinkError::LoadStdLibError(format!(
845 "failed to load manifest in \"{}\"",
846 cargo_manifest_path.display()
847 ))
848 })?;
849 let crate_info = Crate { name: crate_name, entrypoint: lib_entrypoint };
850
851 std_libs.push(crate_info);
852 }
853 }
854
855 Ok(std_libs)
856}
857
858#[allow(clippy::too_many_lines)]
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use indoc::indoc;
863 use module_walker::walk_module_items;
864 use std::cell::RefCell;
865
866 fn item_path(id: &str) -> ItemPath {
867 ItemPath::from_string(id).unwrap()
868 }
869
870 #[test]
871 fn test_item_path_is_toplevel() {
872 assert!(!item_path("crate::baz::mumble").is_toplevel());
873 assert!(!item_path("::std::baz::mumble").is_toplevel());
874 assert!(!item_path("crate::baz").is_toplevel());
875 assert!(!item_path("::std::baz").is_toplevel());
876 assert!(item_path("crate").is_toplevel());
877 assert!(item_path("::std").is_toplevel());
878 }
879
880 #[test]
881 fn test_item_path_parent() {
882 assert_eq!(item_path("crate::baz::mumble").parent(), Some(item_path("crate::baz")));
883 assert_eq!(item_path("::std::baz::mumble").parent(), Some(item_path("::std::baz")));
884 assert_eq!(item_path("crate::baz").parent(), Some(item_path("crate")));
885 assert_eq!(item_path("::std::baz").parent(), Some(item_path("::std")));
886 assert_eq!(item_path("crate").parent(), None);
887 assert_eq!(item_path("::std").parent(), None);
888 }
889
890 #[test]
891 fn test_item_path_join() {
892 assert_eq!(item_path("crate::foo").join(&"bar"), item_path("crate::foo::bar"),);
893 assert_eq!(item_path("::std::foo").join(&"bar"), item_path("::std::foo::bar"),);
894
895 assert_eq!(
896 item_path("::std::foo::bar").parent().unwrap().join(&"baz"),
897 item_path("::std::foo::baz"),
898 );
899 }
900
901 #[test]
902 fn test_all_ancestor_paths() {
903 let symbols = [
904 item_path("crate::foo::bar::baz"),
905 item_path("crate::baz::mumble"),
906 item_path("::std::vec::Vec"),
907 ];
908 let expected: HashSet<ItemPath> = [
909 item_path("crate"),
910 item_path("crate::foo"),
911 item_path("crate::foo::bar"),
912 item_path("crate::baz"),
913 item_path("::std"),
914 item_path("::std::vec"),
915 ]
916 .into_iter()
917 .collect();
918
919 assert_eq!(all_ancestor_paths(symbols.iter()), expected);
920 }
921
922 fn explore_crate(
923 ast: &[Item],
924 dir: &Path,
925 crate_symbol: &ItemPath,
926 should_explore_module: impl Fn(&ItemPath) -> bool,
927 symbols_type: &mut HashMap<ItemPath, SymbolType>,
928 emit_warning: impl Fn(&str),
929 ) {
930 let mut modules_visited: HashSet<ItemPath> = HashSet::new();
931
932 symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
933
934 let mut visit = |module: &ItemPath, item: &Item| {
935 visit_module_item(|_| true, symbols_type, module, item);
936 };
937
938 let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
939 check_explore_module(&should_explore_module, &mut modules_visited, mod_symbol, mod_item)
940 };
941
942 walk_module_items(ast, dir, crate_symbol, &mut visit, &mut explore_module, &emit_warning)
943 .ok()
944 .unwrap();
945 }
946
947 #[test]
948 fn test_walk_module_and_symbols_type() {
949 let module_skip: ItemPath = item_path("crate::skip");
950
951 let source = indoc! { "
952 struct AStruct {}
953
954 mod skip {
955 struct Skip {}
956 }
957
958 mod a {
959 mod b {
960 trait ATrait {}
961 }
962
963 struct FooStruct {}
964 }
965 "
966 };
967
968 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
969 let warnings = RefCell::new(Vec::new());
970
971 explore_crate(
972 &syn::parse_file(source).unwrap().items,
973 &PathBuf::new(),
974 &item_path("crate"),
975 |m| *m != module_skip,
976 &mut symbols_type,
977 |msg| warnings.borrow_mut().push(msg.to_owned()),
978 );
979
980 let expected: HashMap<ItemPath, SymbolType> = [
981 (item_path("crate"), SymbolType::Crate),
982 (item_path("crate::AStruct"), SymbolType::Struct),
983 (item_path("crate::skip"), SymbolType::Mod),
984 (item_path("crate::a"), SymbolType::Mod),
985 (item_path("crate::a::b"), SymbolType::Mod),
986 (item_path("crate::a::b::ATrait"), SymbolType::Trait),
987 (item_path("crate::a::FooStruct"), SymbolType::Struct),
988 ]
989 .into_iter()
990 .collect();
991
992 assert_eq!(symbols_type, expected);
993 }
994
995 #[test]
996 fn test_symbols_type_with_mod_under_cfg_test() {
997 let source = indoc! { "
998 #[cfg(not(test))]
999 mod a {
1000 struct MyStruct {}
1001 }
1002
1003 #[cfg(test)]
1004 mod a {
1005 struct MyStructTest {}
1006 }
1007
1008 #[cfg(test)]
1009 mod b {
1010 struct MyStructTest {}
1011 }
1012
1013 #[cfg(not(test))]
1014 mod b {
1015 struct MyStruct {}
1016 }
1017 "
1018 };
1019
1020 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1021 let warnings = RefCell::new(Vec::new());
1022
1023 explore_crate(
1024 &syn::parse_file(source).unwrap().items,
1025 &PathBuf::new(),
1026 &item_path("crate"),
1027 |_| true,
1028 &mut symbols_type,
1029 |msg| warnings.borrow_mut().push(msg.to_owned()),
1030 );
1031
1032 let expected: HashMap<ItemPath, SymbolType> = [
1033 (item_path("crate"), SymbolType::Crate),
1034 (item_path("crate::a"), SymbolType::Mod),
1035 (item_path("crate::a::MyStruct"), SymbolType::Struct),
1036 (item_path("crate::b"), SymbolType::Mod),
1037 (item_path("crate::b::MyStruct"), SymbolType::Struct),
1038 ]
1039 .into_iter()
1040 .collect();
1041
1042 assert_eq!(symbols_type, expected);
1043 }
1044
1045 #[test]
1046 fn test_symbols_type_multiple_module_first_wins() {
1047 let source = indoc! { "
1048 #[cfg(not(foo))]
1049 mod a {
1050 struct MyStruct {}
1051 }
1052
1053 #[cfg(foo)]
1054 mod a {
1055 struct Skip {}
1056 }
1057 "
1058 };
1059
1060 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1061 let warnings = RefCell::new(Vec::new());
1062
1063 explore_crate(
1064 &syn::parse_file(source).unwrap().items,
1065 &PathBuf::new(),
1066 &item_path("crate"),
1067 |_| true,
1068 &mut symbols_type,
1069 |msg| warnings.borrow_mut().push(msg.to_owned()),
1070 );
1071
1072 let expected: HashMap<ItemPath, SymbolType> = [
1073 (item_path("crate"), SymbolType::Crate),
1074 (item_path("crate::a"), SymbolType::Mod),
1075 (item_path("crate::a::MyStruct"), SymbolType::Struct),
1076 ]
1077 .into_iter()
1078 .collect();
1079
1080 assert_eq!(symbols_type, expected);
1081 }
1082
1083 #[test]
1084 fn test_traverse_module_expore_lazily() {
1085 let symbols: HashSet<ItemPath> = [item_path("crate::module")].into_iter().collect();
1086 let modules = all_ancestor_paths(symbols.iter());
1087
1088 let source = indoc! { "
1089 mod module {
1090 struct Foo {}
1091 }
1092 "
1093 };
1094
1095 let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1096 let warnings = RefCell::new(Vec::new());
1097
1098 explore_crate(
1099 &syn::parse_file(source).unwrap().items,
1100 &PathBuf::new(),
1101 &item_path("crate"),
1102 |module| modules.contains(module),
1103 &mut symbols_type,
1104 |msg| warnings.borrow_mut().push(msg.to_owned()),
1105 );
1106
1107 let symbols_type: HashSet<ItemPath> = symbols_type.keys().cloned().collect();
1108
1109 let expected: HashSet<ItemPath> =
1111 [item_path("crate"), item_path("crate::module")].into_iter().collect();
1112
1113 assert_eq!(symbols_type, expected);
1114 }
1115
1116 #[test]
1117 fn test_documentation_url() {
1118 let config = IntralinksDocsRsConfig::default();
1119
1120 let symbols_type: HashMap<ItemPath, SymbolType> =
1121 [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1122
1123 let link = documentation_url(&item_path("crate"), &symbols_type, "foobini", None, &config);
1124 assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/"));
1125
1126 let symbols_type: HashMap<ItemPath, SymbolType> =
1127 [(item_path("crate::AStruct"), SymbolType::Struct)].into_iter().collect();
1128
1129 let link = documentation_url(
1130 &item_path("crate::AStruct"),
1131 &symbols_type,
1132 "foobini",
1133 None,
1134 &config,
1135 );
1136 assert_eq!(
1137 link.as_deref(),
1138 Some("https://docs.rs/foobini/latest/foobini/struct.AStruct.html")
1139 );
1140
1141 let symbols_type: HashMap<ItemPath, SymbolType> =
1142 [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1143
1144 let link = documentation_url(
1145 &item_path("crate::amodule"),
1146 &symbols_type,
1147 "foobini",
1148 None,
1149 &config,
1150 );
1151 assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/amodule/"));
1152
1153 let symbols_type: HashMap<ItemPath, SymbolType> =
1154 [(item_path("::std"), SymbolType::Crate)].into_iter().collect();
1155
1156 let link = documentation_url(&item_path("::std"), &symbols_type, "foobini", None, &config);
1157 assert_eq!(link.as_deref(), Some("https://doc.rust-lang.org/stable/std/"));
1158
1159 let symbols_type: HashMap<ItemPath, SymbolType> =
1160 [(item_path("::std::collections::HashMap"), SymbolType::Struct)].into_iter().collect();
1161
1162 let link = documentation_url(
1163 &item_path("::std::collections::HashMap"),
1164 &symbols_type,
1165 "foobini",
1166 None,
1167 &config,
1168 );
1169 assert_eq!(
1170 link.as_deref(),
1171 Some("https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html")
1172 );
1173
1174 let symbols_type: HashMap<ItemPath, SymbolType> =
1175 [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1176
1177 let link = documentation_url(
1178 &ItemPath::from_string("crate::amodule").unwrap(),
1179 &symbols_type,
1180 "foo-bar-mumble",
1181 None,
1182 &config,
1183 );
1184 assert_eq!(
1185 link.as_deref(),
1186 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amodule/")
1187 );
1188
1189 let symbols_type: HashMap<ItemPath, SymbolType> =
1190 [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1191
1192 let link = documentation_url(
1193 &ItemPath::from_string("crate").unwrap(),
1194 &symbols_type,
1195 "foo-bar-mumble",
1196 Some("#enums"),
1197 &config,
1198 );
1199 assert_eq!(
1200 link.as_deref(),
1201 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/#enums")
1202 );
1203
1204 let symbols_type: HashMap<ItemPath, SymbolType> =
1205 [(item_path("crate::amod"), SymbolType::Mod)].into_iter().collect();
1206
1207 let link = documentation_url(
1208 &ItemPath::from_string("crate::amod").unwrap(),
1209 &symbols_type,
1210 "foo-bar-mumble",
1211 Some("#structs"),
1212 &config,
1213 );
1214 assert_eq!(
1215 link.as_deref(),
1216 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amod/#structs")
1217 );
1218
1219 let symbols_type: HashMap<ItemPath, SymbolType> =
1220 [(item_path("crate::MyStruct"), SymbolType::Struct)].into_iter().collect();
1221
1222 let link = documentation_url(
1223 &ItemPath::from_string("crate::MyStruct").unwrap(),
1224 &symbols_type,
1225 "foo-bar-mumble",
1226 Some("#implementations"),
1227 &config,
1228 );
1229 assert_eq!(
1230 link.as_deref(),
1231 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/struct.MyStruct.html#implementations")
1232 );
1233
1234 let symbols_type: HashMap<ItemPath, SymbolType> = [
1235 (item_path("crate::mymod::MyStruct"), SymbolType::Struct),
1236 (
1237 item_path("crate::mymod::MyStruct::a_method"),
1238 SymbolType::ImplItem(ImplSymbolType::Method),
1239 ),
1240 ]
1241 .into_iter()
1242 .collect();
1243
1244 let link = documentation_url(
1245 &ItemPath::from_string("crate::mymod::MyStruct::a_method").unwrap(),
1246 &symbols_type,
1247 "foo-bar-mumble",
1248 Some("#thiswillbedropped"),
1249 &config,
1250 );
1251 assert_eq!(
1252 link.as_deref(),
1253 Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/mymod/struct.MyStruct.html#method.a_method")
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}