1use 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
49fn 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#[derive(Clone, Debug)]
74pub struct FQIdentifier {
75 pub anchor: FQIdentifierAnchor,
76
77 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 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 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 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
389fn 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 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 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}