cargo_sync_readme/
intralinks.rs

1//! Tooling to manipulate intralinks by parsing Rust code.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::ops::Range;
7use std::path::{Path, PathBuf};
8use std::rc::Rc;
9use syn::Ident;
10use syn::Item;
11
12use crate::WithWarnings;
13
14#[derive(Debug)]
15pub enum IntraLinkError {
16  IOError(std::io::Error),
17  ParseError(syn::Error),
18  LoadStdLibError(String),
19}
20
21impl fmt::Display for IntraLinkError {
22  fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
23    match self {
24      IntraLinkError::IOError(err) => write!(f, "IO error: {}", err),
25      IntraLinkError::ParseError(err) => write!(f, "Failed to parse rust file: {}", err),
26      IntraLinkError::LoadStdLibError(msg) => write!(f, "Failed to load standard library: {}", msg),
27    }
28  }
29}
30
31impl From<std::io::Error> for IntraLinkError {
32  fn from(err: std::io::Error) -> Self {
33    IntraLinkError::IOError(err)
34  }
35}
36
37impl From<syn::Error> for IntraLinkError {
38  fn from(err: syn::Error) -> Self {
39    IntraLinkError::ParseError(err)
40  }
41}
42
43fn file_ast<P: AsRef<Path>>(filepath: P) -> Result<syn::File, IntraLinkError> {
44  let src = std::fs::read_to_string(filepath)?;
45
46  Ok(syn::parse_file(&src)?)
47}
48
49/// Determines the module filename, which can be `<module>.rs` or `<module>/mod.rs`.
50fn module_filename(dir: &Path, module: &Ident) -> Option<PathBuf> {
51  let mod_file = dir.join(format!("{}.rs", module));
52
53  if mod_file.is_file() {
54    return Some(mod_file);
55  }
56
57  let mod_file = dir.join(module.to_string()).join("mod.rs");
58
59  if mod_file.is_file() {
60    return Some(mod_file);
61  }
62
63  None
64}
65
66#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
67pub enum FQIdentifierAnchor {
68  Root,
69  Crate,
70}
71
72/// Fully qualified identifier.
73#[derive(Clone, Debug)]
74pub struct FQIdentifier {
75  pub anchor: FQIdentifierAnchor,
76
77  /// This path vector can be shared and can end after the identifier we are representing.
78  /// This allow us to have a faster implementation for `FQIdentifier::all_ancestors()`.
79  path_shared: Rc<Vec<String>>,
80  path_end: usize,
81}
82
83impl FQIdentifier {
84  fn new(anchor: FQIdentifierAnchor) -> FQIdentifier {
85    FQIdentifier {
86      anchor,
87      path_shared: Rc::new(Vec::new()),
88      path_end: 0,
89    }
90  }
91
92  fn root(crate_name: &str) -> FQIdentifier {
93    FQIdentifier::new(FQIdentifierAnchor::Root).join(crate_name)
94  }
95
96  fn from_string(s: &str) -> Option<FQIdentifier> {
97    let anchor;
98    let rest;
99
100    if s == "::" {
101      return Some(FQIdentifier::new(FQIdentifierAnchor::Root));
102    } else if s.starts_with("::") {
103      anchor = FQIdentifierAnchor::Root;
104      rest = &s[2..];
105    } else if s == "crate" {
106      return Some(FQIdentifier::new(FQIdentifierAnchor::Crate));
107    } else if s.starts_with("crate::") {
108      anchor = FQIdentifierAnchor::Crate;
109      rest = &s[7..];
110    } else {
111      return None;
112    }
113
114    if rest.is_empty() {
115      return None;
116    }
117
118    let path: Rc<Vec<String>> = Rc::new(rest.split("::").map(str::to_owned).collect());
119
120    Some(FQIdentifier {
121      anchor,
122      path_end: path.len(),
123      path_shared: path,
124    })
125  }
126
127  fn path(&self) -> &[String] {
128    &self.path_shared[0..self.path_end]
129  }
130
131  fn parent(mut self) -> Option<FQIdentifier> {
132    match self.path_end {
133      0 => None,
134      _ => {
135        self.path_end -= 1;
136        Some(self)
137      }
138    }
139  }
140
141  fn join(mut self, s: &str) -> FQIdentifier {
142    Rc::make_mut(&mut self.path_shared).push(s.to_string());
143    self.path_end += 1;
144    self
145  }
146
147  fn all_ancestors(&self) -> impl Iterator<Item = FQIdentifier> {
148    let first_ancestor = self.clone().parent();
149
150    std::iter::successors(first_ancestor, |ancestor| ancestor.clone().parent())
151  }
152}
153
154impl PartialEq for FQIdentifier {
155  fn eq(&self, other: &Self) -> bool {
156    self.anchor == other.anchor && self.path() == other.path()
157  }
158}
159
160impl Eq for FQIdentifier {}
161
162impl Hash for FQIdentifier {
163  fn hash<H: Hasher>(&self, state: &mut H) {
164    self.anchor.hash(state);
165    self.path().hash(state);
166  }
167}
168
169impl fmt::Display for FQIdentifier {
170  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
171    match self.anchor {
172      FQIdentifierAnchor::Root => (),
173      FQIdentifierAnchor::Crate => f.write_str("crate")?,
174    }
175
176    for s in self.path().iter() {
177      f.write_str("::")?;
178      f.write_str(&s)?;
179    }
180
181    Ok(())
182  }
183}
184
185fn is_cfg_test(attribute: &syn::Attribute) -> bool {
186  let test_attribute: syn::Attribute = syn::parse_quote!(#[cfg(test)]);
187
188  *attribute == test_attribute
189}
190
191fn traverse_module(
192  ast: &Vec<Item>,
193  dir: &Path,
194  mod_symbol: FQIdentifier,
195  asts: &mut HashMap<FQIdentifier, Vec<Item>>,
196  explore_module: &impl Fn(&FQIdentifier) -> bool,
197  warnings: &mut Vec<String>,
198) -> Result<(), IntraLinkError> {
199  if !explore_module(&mod_symbol) {
200    return Ok(());
201  }
202
203  // Conditional compilation can create multiple module definitions, e.g.
204  //
205  // ```
206  // #[cfg(foo)]
207  // mod a {}
208  // #[cfg(not(foo))]
209  // mod a {}
210  // ```
211  //
212  // We choose to consider the first one only.
213  match asts.contains_key(&mod_symbol) {
214    true => return Ok(()),
215    false => asts.insert(mod_symbol.clone(), ast.clone()),
216  };
217
218  for item in ast.iter() {
219    if let Item::Mod(module) = item {
220      // If a module is gated by `#[cfg(test)]` we skip it.  This happens sometimes in the
221      // standard library, and we want to explore the correct, non-test, module.
222      if module.attrs.iter().any(is_cfg_test) {
223        continue;
224      }
225
226      let child_module_symbol: FQIdentifier = mod_symbol.clone().join(&module.ident.to_string());
227
228      match &module.content {
229        Some((_, items)) => {
230          traverse_module(
231            &items,
232            dir,
233            child_module_symbol,
234            asts,
235            explore_module,
236            warnings,
237          )?;
238        }
239        None if explore_module(&child_module_symbol) => match module_filename(dir, &module.ident) {
240          None => warnings.push(format!(
241            "Unable to find module file for module {} in directory {:?}",
242            child_module_symbol, dir
243          )),
244          Some(mod_filename) => traverse_file(
245            mod_filename,
246            child_module_symbol,
247            asts,
248            explore_module,
249            warnings,
250          )?,
251        },
252        None => (),
253      }
254    }
255  }
256
257  Ok(())
258}
259
260fn traverse_file<P: AsRef<Path>>(
261  file: P,
262  mod_symbol: FQIdentifier,
263  asts: &mut HashMap<FQIdentifier, Vec<Item>>,
264  explore_module: &impl Fn(&FQIdentifier) -> bool,
265  warnings: &mut Vec<String>,
266) -> Result<(), IntraLinkError> {
267  let dir: &Path = file.as_ref().parent().expect(&format!(
268    "failed to get directory of \"{:?}\"",
269    file.as_ref()
270  ));
271  let ast: syn::File = file_ast(&file)?;
272
273  traverse_module(&ast.items, dir, mod_symbol, asts, explore_module, warnings)
274}
275
276#[derive(Copy, Clone, Debug, PartialEq, Eq)]
277pub enum SymbolType {
278  Crate,
279  Struct,
280  Trait,
281  Enum,
282  Union,
283  Type,
284  Mod,
285  Macro,
286  Const,
287  Fn,
288  Static,
289}
290
291fn symbols_type(asts: &HashMap<FQIdentifier, Vec<Item>>) -> HashMap<FQIdentifier, SymbolType> {
292  let mut symbols_type: HashMap<FQIdentifier, SymbolType> = HashMap::new();
293
294  for (mod_symbol, ast) in asts.iter() {
295    if *mod_symbol == FQIdentifier::new(FQIdentifierAnchor::Crate) {
296      symbols_type.insert(
297        FQIdentifier::new(FQIdentifierAnchor::Crate),
298        SymbolType::Crate,
299      );
300    }
301
302    for item in ast {
303      let mut symb_type: Option<(String, SymbolType)> = None;
304
305      match item {
306        Item::Enum(e) => {
307          symb_type = Some((e.ident.to_string(), SymbolType::Enum));
308        }
309        Item::Struct(s) => {
310          symb_type = Some((s.ident.to_string(), SymbolType::Struct));
311        }
312        Item::Trait(t) => {
313          symb_type = Some((t.ident.to_string(), SymbolType::Trait));
314        }
315        Item::Union(u) => {
316          symb_type = Some((u.ident.to_string(), SymbolType::Union));
317        }
318        Item::Type(t) => {
319          symb_type = Some((t.ident.to_string(), SymbolType::Type));
320        }
321        Item::Mod(m) => {
322          symb_type = Some((m.ident.to_string(), SymbolType::Mod));
323        }
324        Item::Macro(syn::ItemMacro {
325          ident: Some(ident), ..
326        }) => {
327          symb_type = Some((ident.to_string(), SymbolType::Macro));
328        }
329        Item::Macro2(m) => {
330          symb_type = Some((m.ident.to_string(), SymbolType::Macro));
331        }
332        Item::Const(c) => {
333          symb_type = Some((c.ident.to_string(), SymbolType::Const));
334        }
335        Item::Fn(f) => {
336          symb_type = Some((f.sig.ident.to_string(), SymbolType::Fn));
337        }
338        Item::Static(s) => {
339          symb_type = Some((s.ident.to_string(), SymbolType::Static));
340        }
341
342        _ => (),
343      }
344
345      if let Some((ident, typ)) = symb_type {
346        symbols_type.insert(mod_symbol.clone().join(&ident), typ);
347      }
348    }
349  }
350
351  symbols_type
352}
353
354pub fn crate_symbols_type<P: AsRef<Path>>(
355  entry_point: P,
356  symbols: &HashSet<FQIdentifier>,
357  warnings: &mut Vec<String>,
358) -> Result<HashMap<FQIdentifier, SymbolType>, IntraLinkError> {
359  let modules = all_supermodules(symbols.iter());
360  let mut asts: HashMap<FQIdentifier, Vec<Item>> = HashMap::new();
361
362  // Only load standard library information if needed.
363  let std_lib_crates = match references_standard_library(&symbols) {
364    true => get_standard_libraries()?,
365    false => Vec::new(),
366  };
367
368  for Crate { name, entrypoint } in std_lib_crates {
369    traverse_file(
370      entrypoint,
371      FQIdentifier::root(&name),
372      &mut asts,
373      &|module| modules.contains(module),
374      warnings,
375    )?;
376  }
377
378  traverse_file(
379    entry_point,
380    FQIdentifier::new(FQIdentifierAnchor::Crate),
381    &mut asts,
382    &|module| modules.contains(module),
383    warnings,
384  )?;
385
386  Ok(symbols_type(&asts))
387}
388
389/// Create a set with all supermodules in `symbols`.  For instance, if `symbols` is
390/// `{crate::foo::bar::baz, crate::baz::mumble}` it will return
391/// `{crate, crate::foo, crate::foo::bar, crate::baz}`.
392fn all_supermodules<'a>(symbols: impl Iterator<Item = &'a FQIdentifier>) -> HashSet<FQIdentifier> {
393  symbols
394    .into_iter()
395    .flat_map(|s| s.all_ancestors())
396    .collect()
397}
398
399#[derive(Eq, PartialEq, Debug)]
400struct MarkdownInlineLink {
401  text: String,
402  link: String,
403}
404
405fn split_link_fragment(link: &str) -> (&str, &str) {
406  match link.find('#') {
407    None => (link, ""),
408    Some(i) => link.split_at(i),
409  }
410}
411
412fn markdown_inline_link_iterator<'a>(
413  source: &'a str,
414) -> impl Iterator<Item = (Span, MarkdownInlineLink)> + 'a {
415  use pulldown_cmark::*;
416
417  fn escape_markdown(str: &str, escape_chars: &str) -> String {
418    let mut s = String::new();
419
420    for c in str.chars() {
421      match escape_chars.contains(c) {
422        true => {
423          s.push('\\');
424          s.push(c);
425        }
426        false => s.push(c),
427      }
428    }
429
430    s
431  }
432
433  let parser = Parser::new_ext(source, Options::all());
434  let mut in_link = false;
435  let mut text = String::new();
436
437  parser
438    .into_offset_iter()
439    .filter_map(move |(event, range)| match event {
440      Event::Start(Tag::Link(LinkType::Inline, ..)) => {
441        in_link = true;
442        None
443      }
444      Event::End(Tag::Link(LinkType::Inline, link, ..)) => {
445        in_link = false;
446
447        let t: String = escape_markdown(&std::mem::take(&mut text), r"\[]");
448        let l: String = escape_markdown(link.as_ref(), r"\()");
449
450        Some((range.into(), MarkdownInlineLink { text: t, link: l }))
451      }
452      Event::Text(s) if in_link => {
453        text.push_str(&s);
454        None
455      }
456      Event::Code(s) if in_link => {
457        text.push('`');
458        text.push_str(&s);
459        text.push('`');
460        None
461      }
462      _ => None,
463    })
464}
465
466#[derive(Eq, PartialEq, Debug)]
467pub struct Span {
468  pub start: usize,
469  pub end: usize,
470}
471
472impl From<Range<usize>> for Span {
473  fn from(range: Range<usize>) -> Self {
474    Span {
475      start: range.start,
476      end: range.end,
477    }
478  }
479}
480
481pub fn extract_markdown_intralink_symbols(doc: &str) -> HashSet<FQIdentifier> {
482  let mut symbols = HashSet::new();
483
484  for (_, MarkdownInlineLink { link, .. }) in markdown_inline_link_iterator(doc) {
485    let (link, _) = split_link_fragment(&link);
486
487    if let Some(symbol) = FQIdentifier::from_string(&link) {
488      symbols.insert(symbol);
489    }
490  }
491
492  symbols
493}
494
495fn documentation_url(symbol: &FQIdentifier, typ: SymbolType, crate_name: &str) -> String {
496  let mut link = match symbol.anchor {
497    FQIdentifierAnchor::Root => format!("https://doc.rust-lang.org/stable/"),
498    FQIdentifierAnchor::Crate => format!("https://docs.rs/{}/latest/{}/", crate_name, crate_name),
499  };
500
501  if SymbolType::Crate == typ {
502    return link;
503  }
504
505  for s in symbol.path().iter().rev().skip(1).rev() {
506    link.push_str(s);
507    link.push('/');
508  }
509
510  let name = symbol
511    .path()
512    .last()
513    .expect(&format!("failed to get last component of {}", symbol));
514
515  match typ {
516    SymbolType::Crate => unreachable!(),
517    SymbolType::Struct => link.push_str(&format!("struct.{}.html", name)),
518    SymbolType::Trait => link.push_str(&format!("trait.{}.html", name)),
519    SymbolType::Enum => link.push_str(&format!("enum.{}.html", name)),
520    SymbolType::Union => link.push_str(&format!("union.{}.html", name)),
521    SymbolType::Type => link.push_str(&format!("type.{}.html", name)),
522    SymbolType::Mod => link.push_str(&format!("{}/", name)),
523    SymbolType::Macro => link.push_str(&format!("macro.{}.html", name)),
524    SymbolType::Const => link.push_str(&format!("const.{}.html", name)),
525    SymbolType::Fn => link.push_str(&format!("fn.{}.html", name)),
526    SymbolType::Static => link.push_str(&format!("static.{}.html", name)),
527  }
528
529  link
530}
531
532pub fn rewrite_markdown_links(
533  doc: &str,
534  symbols_type: &HashMap<FQIdentifier, SymbolType>,
535  crate_name: &str,
536  mut warnings: Vec<String>,
537) -> WithWarnings<String> {
538  let mut new_doc = String::with_capacity(doc.len());
539  let mut last_span = Span { start: 0, end: 0 };
540
541  for (span, link) in markdown_inline_link_iterator(doc) {
542    new_doc.push_str(&doc[last_span.end..span.start]);
543
544    let MarkdownInlineLink { text, link } = link;
545    let (link, fragment): (&str, &str) = split_link_fragment(&link);
546
547    match FQIdentifier::from_string(&link) {
548      Some(symbol) if symbols_type.contains_key(&symbol) => {
549        let typ = symbols_type[&symbol];
550        let new_link = documentation_url(&symbol, typ, crate_name);
551
552        new_doc.push_str(&format!("[{}]({}{})", text, new_link, fragment));
553      }
554
555      r => {
556        if let Some(symbol) = r {
557          warnings.push(format!("Could not find definition of `{}`.", symbol));
558        }
559
560        new_doc.push_str(&format!("[{}]({}{})", text, link, fragment));
561      }
562    }
563
564    last_span = span;
565  }
566
567  new_doc.push_str(&doc[last_span.end..]);
568
569  WithWarnings::new(new_doc, warnings)
570}
571
572fn get_rustc_sysroot_libraries_dir() -> Result<PathBuf, IntraLinkError> {
573  use std::process::*;
574
575  let output = Command::new("rustc")
576    .args(&["--print=sysroot"])
577    .output()
578    .map_err(|e| IntraLinkError::LoadStdLibError(format!("failed to run rustc: {}", e)))?;
579
580  let s = String::from_utf8(output.stdout).expect("unexpected output from rustc");
581  let sysroot = PathBuf::from(s.trim());
582  let src_path = sysroot
583    .join("lib")
584    .join("rustlib")
585    .join("src")
586    .join("rust")
587    .join("library");
588
589  match src_path.is_dir() {
590    false => Err(IntraLinkError::LoadStdLibError(format!(
591      "\"{:?}\" is not a directory",
592      src_path
593    ))),
594    true => Ok(src_path),
595  }
596}
597
598#[derive(Debug)]
599struct Crate {
600  name: String,
601  entrypoint: PathBuf,
602}
603
604fn references_standard_library(symbols: &HashSet<FQIdentifier>) -> bool {
605  // The only way to reference standard libraries that we support is with a intra-link of form `::⋯`.
606  symbols
607    .iter()
608    .any(|symbol| symbol.anchor == FQIdentifierAnchor::Root)
609}
610
611fn get_standard_libraries() -> Result<Vec<Crate>, IntraLinkError> {
612  let libraries_dir = get_rustc_sysroot_libraries_dir()?;
613  let mut std_libs = Vec::with_capacity(32);
614
615  for entry in std::fs::read_dir(&libraries_dir)? {
616    let entry = entry?;
617    let path = entry.path();
618    let cargo_manifest = path.join("Cargo.toml");
619    let lib_entrypoint = path.join("src").join("lib.rs");
620
621    if cargo_manifest.is_file() && lib_entrypoint.is_file() {
622      let crate_name = super::Manifest::load(&cargo_manifest)
623        .map_err(|e| {
624          IntraLinkError::LoadStdLibError(format!(
625            "failed to load manifest in \"{:?}\": {}",
626            cargo_manifest, e
627          ))
628        })?
629        .crate_name()
630        .ok_or_else(|| {
631          IntraLinkError::LoadStdLibError(format!(
632            "cannot get crate name from \"{:?}\"",
633            cargo_manifest
634          ))
635        })?
636        .to_owned();
637      let crate_info = Crate {
638        name: crate_name.to_owned(),
639        entrypoint: lib_entrypoint,
640      };
641
642      std_libs.push(crate_info);
643    }
644  }
645
646  Ok(std_libs)
647}
648
649#[cfg(test)]
650mod tests {
651  use super::*;
652
653  #[test]
654  fn test_markdown_link_iterator() {
655    let markdown = "A [some text] [another](http://foo.com), [another][one]";
656
657    let mut iter = markdown_inline_link_iterator(&markdown);
658    let (Span { start, end }, link) = iter.next().unwrap();
659    assert_eq!(
660      link,
661      MarkdownInlineLink {
662        text: "another".to_owned(),
663        link: "http://foo.com".to_owned(),
664      }
665    );
666    assert_eq!(&markdown[start..end], "[another](http://foo.com)");
667
668    assert_eq!(iter.next(), None);
669
670    let markdown = "[another](http://foo.com)[another][one]";
671    let mut iter = markdown_inline_link_iterator(&markdown);
672
673    let (Span { start, end }, link) = iter.next().unwrap();
674    assert_eq!(
675      link,
676      MarkdownInlineLink {
677        text: "another".to_owned(),
678        link: "http://foo.com".to_owned(),
679      }
680    );
681    assert_eq!(&markdown[start..end], "[another](http://foo.com)");
682
683    assert_eq!(iter.next(), None);
684
685    let markdown = "A [some [text]], [another [text] (foo)](http://foo.com/foo(bar)), [another [] one][foo[]bar]";
686    let mut iter = markdown_inline_link_iterator(&markdown);
687
688    let (Span { start, end }, link) = iter.next().unwrap();
689    assert_eq!(
690      link,
691      MarkdownInlineLink {
692        text: r"another \[text\] (foo)".to_owned(),
693        link: r"http://foo.com/foo\(bar\)".to_owned(),
694      }
695    );
696    assert_eq!(
697      &markdown[start..end],
698      "[another [text] (foo)](http://foo.com/foo(bar))"
699    );
700
701    assert_eq!(iter.next(), None);
702
703    let markdown = "A [some \\]text], [another](http://foo.\\(com\\)), [another\\]][one\\]]";
704    let mut iter = markdown_inline_link_iterator(&markdown);
705
706    let (Span { start, end }, link) = iter.next().unwrap();
707    assert_eq!(
708      link,
709      MarkdownInlineLink {
710        text: "another".to_owned(),
711        link: r"http://foo.\(com\)".to_owned(),
712      }
713    );
714    assert_eq!(&markdown[start..end], r"[another](http://foo.\(com\))");
715
716    assert_eq!(iter.next(), None);
717
718    let markdown = "A `this is no link [link](http://foo.com)`";
719    let mut iter = markdown_inline_link_iterator(&markdown);
720
721    assert_eq!(iter.next(), None);
722
723    let markdown = "A\n```\nthis is no link [link](http://foo.com)\n```";
724    let mut iter = markdown_inline_link_iterator(&markdown);
725
726    assert_eq!(iter.next(), None);
727
728    let markdown = "A [link with `code`!](http://foo.com)!";
729    let mut iter = markdown_inline_link_iterator(&markdown);
730
731    let (Span { start, end }, link) = iter.next().unwrap();
732    assert_eq!(
733      link,
734      MarkdownInlineLink {
735        text: "link with `code`!".to_owned(),
736        link: "http://foo.com".to_owned(),
737      }
738    );
739    assert_eq!(&markdown[start..end], "[link with `code`!](http://foo.com)");
740
741    assert_eq!(iter.next(), None);
742  }
743
744  #[test]
745  fn test_all_supermodules() {
746    let symbols = [
747      FQIdentifier::from_string("crate::foo::bar::baz").unwrap(),
748      FQIdentifier::from_string("crate::baz::mumble").unwrap(),
749    ];
750    let expected: HashSet<FQIdentifier> = [
751      FQIdentifier::from_string("crate").unwrap(),
752      FQIdentifier::from_string("crate::foo").unwrap(),
753      FQIdentifier::from_string("crate::foo::bar").unwrap(),
754      FQIdentifier::from_string("crate::baz").unwrap(),
755    ]
756    .iter()
757    .cloned()
758    .collect();
759
760    assert_eq!(all_supermodules(symbols.iter()), expected);
761  }
762
763  #[test]
764  fn test_traverse_module_and_symbols_type() {
765    let mut asts: HashMap<FQIdentifier, Vec<Item>> = HashMap::new();
766    let module_skip: FQIdentifier = FQIdentifier::from_string("crate::skip").unwrap();
767
768    let source = "
769        struct AStruct {}
770
771        mod skip {
772          struct Skip {}
773        }
774
775        mod a {
776          mod b {
777            trait ATrait {}
778          }
779
780          struct FooStruct {}
781        }
782        ";
783
784    let mut warnings = Vec::new();
785    traverse_module(
786      &syn::parse_file(&source).unwrap().items,
787      &PathBuf::new(),
788      FQIdentifier::new(FQIdentifierAnchor::Crate),
789      &mut asts,
790      &|m| *m != module_skip,
791      &mut warnings,
792    )
793    .ok()
794    .unwrap();
795
796    let symbols_type: HashMap<FQIdentifier, SymbolType> = symbols_type(&asts);
797    let expected: HashMap<FQIdentifier, SymbolType> = [
798      (
799        FQIdentifier::from_string("crate").unwrap(),
800        SymbolType::Crate,
801      ),
802      (
803        FQIdentifier::from_string("crate::AStruct").unwrap(),
804        SymbolType::Struct,
805      ),
806      (
807        FQIdentifier::from_string("crate::skip").unwrap(),
808        SymbolType::Mod,
809      ),
810      (
811        FQIdentifier::from_string("crate::a").unwrap(),
812        SymbolType::Mod,
813      ),
814      (
815        FQIdentifier::from_string("crate::a::b").unwrap(),
816        SymbolType::Mod,
817      ),
818      (
819        FQIdentifier::from_string("crate::a::b::ATrait").unwrap(),
820        SymbolType::Trait,
821      ),
822      (
823        FQIdentifier::from_string("crate::a::FooStruct").unwrap(),
824        SymbolType::Struct,
825      ),
826    ]
827    .iter()
828    .cloned()
829    .collect();
830
831    assert_eq!(symbols_type, expected)
832  }
833
834  #[test]
835  fn test_symbols_type_with_mod_under_cfg_test() {
836    let mut asts: HashMap<FQIdentifier, Vec<Item>> = HashMap::new();
837
838    let source = "
839        #[cfg(not(test))]
840        mod a {
841          struct MyStruct {}
842        }
843
844        #[cfg(test)]
845        mod a {
846          struct MyStructTest {}
847        }
848
849        #[cfg(test)]
850        mod b {
851          struct MyStructTest {}
852        }
853
854        #[cfg(not(test))]
855        mod b {
856          struct MyStruct {}
857        }
858        ";
859
860    let mut warnings = Vec::new();
861    traverse_module(
862      &syn::parse_file(&source).unwrap().items,
863      &PathBuf::new(),
864      FQIdentifier::new(FQIdentifierAnchor::Crate),
865      &mut asts,
866      &|_| true,
867      &mut warnings,
868    )
869    .ok()
870    .unwrap();
871
872    let symbols_type: HashMap<FQIdentifier, SymbolType> = symbols_type(&asts);
873    let expected: HashMap<FQIdentifier, SymbolType> = [
874      (
875        FQIdentifier::from_string("crate").unwrap(),
876        SymbolType::Crate,
877      ),
878      (
879        FQIdentifier::from_string("crate::a").unwrap(),
880        SymbolType::Mod,
881      ),
882      (
883        FQIdentifier::from_string("crate::a::MyStruct").unwrap(),
884        SymbolType::Struct,
885      ),
886      (
887        FQIdentifier::from_string("crate::b").unwrap(),
888        SymbolType::Mod,
889      ),
890      (
891        FQIdentifier::from_string("crate::b::MyStruct").unwrap(),
892        SymbolType::Struct,
893      ),
894    ]
895    .iter()
896    .cloned()
897    .collect();
898
899    assert_eq!(symbols_type, expected)
900  }
901
902  #[test]
903  fn test_symbols_type_multiple_module_first_wins() {
904    let mut asts: HashMap<FQIdentifier, Vec<Item>> = HashMap::new();
905
906    let source = "
907        #[cfg(not(foo))]
908        mod a {
909          struct MyStruct {}
910        }
911
912        #[cfg(foo)]
913        mod a {
914          struct Skip {}
915        }
916        ";
917
918    let mut warnings = Vec::new();
919    traverse_module(
920      &syn::parse_file(&source).unwrap().items,
921      &PathBuf::new(),
922      FQIdentifier::new(FQIdentifierAnchor::Crate),
923      &mut asts,
924      &|_| true,
925      &mut warnings,
926    )
927    .ok()
928    .unwrap();
929
930    let symbols_type: HashMap<FQIdentifier, SymbolType> = symbols_type(&asts);
931    let expected: HashMap<FQIdentifier, SymbolType> = [
932      (
933        FQIdentifier::from_string("crate").unwrap(),
934        SymbolType::Crate,
935      ),
936      (
937        FQIdentifier::from_string("crate::a").unwrap(),
938        SymbolType::Mod,
939      ),
940      (
941        FQIdentifier::from_string("crate::a::MyStruct").unwrap(),
942        SymbolType::Struct,
943      ),
944    ]
945    .iter()
946    .cloned()
947    .collect();
948
949    assert_eq!(symbols_type, expected)
950  }
951
952  #[test]
953  fn test_traverse_module_expore_lazily() {
954    let symbols: HashSet<FQIdentifier> = [FQIdentifier::from_string("crate::module").unwrap()]
955      .iter()
956      .cloned()
957      .collect();
958    let modules = all_supermodules(symbols.iter());
959
960    let mut asts: HashMap<FQIdentifier, Vec<Item>> = HashMap::new();
961
962    let source = "
963        mod module {
964          struct Foo {}
965        }
966        ";
967
968    let mut warnings = Vec::new();
969    traverse_module(
970      &syn::parse_file(&source).unwrap().items,
971      &PathBuf::new(),
972      FQIdentifier::new(FQIdentifierAnchor::Crate),
973      &mut asts,
974      &|module| modules.contains(module),
975      &mut warnings,
976    )
977    .ok()
978    .unwrap();
979
980    let symbols_type: HashSet<FQIdentifier> = symbols_type(&asts).keys().cloned().collect();
981
982    // We should still get `crate::module`, but nothing inside it.
983    let expected: HashSet<FQIdentifier> = [
984      FQIdentifier::from_string("crate").unwrap(),
985      FQIdentifier::from_string("crate::module").unwrap(),
986    ]
987    .iter()
988    .cloned()
989    .collect();
990
991    assert_eq!(symbols_type, expected);
992  }
993
994  #[test]
995  fn test_documentation_url() {
996    let link = documentation_url(
997      &FQIdentifier::from_string("crate").unwrap(),
998      SymbolType::Crate,
999      "foobini",
1000    );
1001    assert_eq!(link, "https://docs.rs/foobini/latest/foobini/");
1002
1003    let link = documentation_url(
1004      &FQIdentifier::from_string("crate::AStruct").unwrap(),
1005      SymbolType::Struct,
1006      "foobini",
1007    );
1008    assert_eq!(
1009      link,
1010      "https://docs.rs/foobini/latest/foobini/struct.AStruct.html"
1011    );
1012
1013    let link = documentation_url(
1014      &FQIdentifier::from_string("crate::amodule").unwrap(),
1015      SymbolType::Mod,
1016      "foobini",
1017    );
1018    assert_eq!(link, "https://docs.rs/foobini/latest/foobini/amodule/");
1019  }
1020
1021  #[test]
1022  fn test_extract_markdown_intralink_symbols() {
1023    let doc = "
1024# Foobini
1025
1026This [this crate](crate) is cool because it contains [modules](crate::amodule) and some
1027other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1028
1029Go ahead and check all the [structs in foo](crate::foo#structs).
1030Also check [this](::std::sync::Arc) and [this](::alloc::sync::Arc).
1031";
1032
1033    let symbols = extract_markdown_intralink_symbols(doc);
1034
1035    let expected: HashSet<FQIdentifier> = [
1036      FQIdentifier::from_string("crate").unwrap(),
1037      FQIdentifier::from_string("crate::amodule").unwrap(),
1038      FQIdentifier::from_string("crate::foo").unwrap(),
1039      FQIdentifier::from_string("::std::sync::Arc").unwrap(),
1040      FQIdentifier::from_string("::alloc::sync::Arc").unwrap(),
1041    ]
1042    .iter()
1043    .cloned()
1044    .collect();
1045
1046    assert_eq!(symbols, expected);
1047  }
1048
1049  #[test]
1050  fn test_rewrite_markdown_links() {
1051    let doc = r"
1052# Foobini
1053
1054This [this crate](crate) is cool because it contains [modules](crate::amodule) and some
1055other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1056
1057This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
1058should [wor\\k \[fi\]le](f\\i\(n\)e).
1059
1060Go ahead and check all the [structs in foo](crate::foo#structs) specifically
1061[this one](crate::foo::BestStruct)
1062";
1063
1064    let symbols_type: HashMap<FQIdentifier, SymbolType> = [
1065      (
1066        FQIdentifier::from_string("crate").unwrap(),
1067        SymbolType::Crate,
1068      ),
1069      (
1070        FQIdentifier::from_string("crate::amodule").unwrap(),
1071        SymbolType::Mod,
1072      ),
1073      (
1074        FQIdentifier::from_string("crate::foo").unwrap(),
1075        SymbolType::Mod,
1076      ),
1077      (
1078        FQIdentifier::from_string("crate::foo::BestStruct").unwrap(),
1079        SymbolType::Struct,
1080      ),
1081    ]
1082    .iter()
1083    .cloned()
1084    .collect();
1085
1086    let WithWarnings {
1087      value: new_readme, ..
1088    } = rewrite_markdown_links(&doc, &symbols_type, "foobini", Vec::new());
1089    let expected = r"
1090# Foobini
1091
1092This [this crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/) and some
1093other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1094
1095This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
1096should [wor\\k \[fi\]le](f\\i\(n\)e).
1097
1098Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically
1099[this one](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
1100";
1101
1102    assert_eq!(new_readme, expected);
1103  }
1104}