use crate::transform::intralinks::links::{
markdown_link_iterator, markdown_reference_link_definition_iterator, Link, MarkdownLink,
};
use crate::transform::DocTransform;
use crate::Doc;
use module_walker::walk_module_file;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use syn::{Item, ItemMod};
use thiserror::Error;
use unicase::UniCase;
mod links;
mod module_walker;
#[derive(Error, Debug)]
pub enum IntralinkError {
#[error("IO error: {0}")]
IOError(std::io::Error),
#[error("failed to analyzing code: {0}")]
AstWalkError(module_walker::ModuleWalkError),
#[error("failed to load standard library: {0}")]
LoadStdLibError(String),
}
impl From<std::io::Error> for IntralinkError {
fn from(err: std::io::Error) -> Self {
IntralinkError::IOError(err)
}
}
impl From<module_walker::ModuleWalkError> for IntralinkError {
fn from(err: module_walker::ModuleWalkError) -> Self {
IntralinkError::AstWalkError(err)
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct IntralinksDocsRsConfig {
pub docs_rs_base_url: Option<String>,
pub docs_rs_version: Option<String>,
}
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct IntralinksConfig {
pub docs_rs: IntralinksDocsRsConfig,
pub strip_links: Option<bool>,
}
pub struct DocTransformIntralinks<F> {
crate_name: String,
entrypoint: PathBuf,
emit_warning: F,
config: IntralinksConfig,
}
impl<F> DocTransformIntralinks<F>
where
F: Fn(&str),
{
pub fn new(
crate_name: impl Into<String>,
entrypoint: impl AsRef<Path>,
emit_warning: F,
config: Option<IntralinksConfig>,
) -> DocTransformIntralinks<F> {
DocTransformIntralinks {
crate_name: crate_name.into(),
entrypoint: entrypoint.as_ref().to_path_buf(),
emit_warning,
config: config.unwrap_or_default(),
}
}
}
impl<F> DocTransform for DocTransformIntralinks<F>
where
F: Fn(&str),
{
type E = IntralinkError;
fn transform(&self, doc: &Doc) -> Result<Doc, IntralinkError> {
let symbols: HashSet<ItemPath> = extract_markdown_intralink_symbols(doc);
if symbols.is_empty() {
return Ok(doc.clone());
}
let symbols_type = match self.config.strip_links.unwrap_or(false) {
false => load_symbols_type(&self.entrypoint, &symbols, &self.emit_warning)?,
true => HashMap::new(),
};
let doc =
rewrite_links(doc, &symbols_type, &self.crate_name, &self.emit_warning, &self.config);
Ok(doc)
}
}
fn rewrite_links(
doc: &Doc,
symbols_type: &HashMap<ItemPath, SymbolType>,
crate_name: &str,
emit_warning: &impl Fn(&str),
config: &IntralinksConfig,
) -> Doc {
let RewriteReferenceLinksResult { doc, reference_links_to_remove } =
rewrite_reference_links_definitions(doc, symbols_type, crate_name, emit_warning, config);
let doc = rewrite_markdown_links(
&doc,
symbols_type,
crate_name,
emit_warning,
config,
&reference_links_to_remove,
);
doc
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum ItemPathAnchor {
Root,
Crate,
}
#[derive(Clone)]
pub struct ItemPath {
pub anchor: ItemPathAnchor,
path_shared: Rc<Vec<String>>,
path_end: usize,
}
impl ItemPath {
fn new(anchor: ItemPathAnchor) -> ItemPath {
ItemPath { anchor, path_shared: Rc::new(Vec::new()), path_end: 0 }
}
fn root(crate_name: &str) -> ItemPath {
ItemPath::new(ItemPathAnchor::Root).join(&crate_name)
}
fn from_string(s: &str) -> Option<ItemPath> {
let anchor;
let rest;
if let Some(r) = s.strip_prefix("::") {
anchor = ItemPathAnchor::Root;
rest = r;
} else if s == "crate" {
return Some(ItemPath::new(ItemPathAnchor::Crate));
} else if let Some(r) = s.strip_prefix("crate::") {
anchor = ItemPathAnchor::Crate;
rest = r;
} else {
return None;
}
if rest.is_empty() {
return None;
}
let path: Rc<Vec<String>> = Rc::new(rest.split("::").map(str::to_owned).collect());
Some(ItemPath { anchor, path_end: path.len(), path_shared: path })
}
fn path_components(&self) -> impl Iterator<Item = &str> {
self.path_shared[0..self.path_end].iter().map(String::as_str)
}
fn is_toplevel(&self) -> bool {
match self.anchor {
ItemPathAnchor::Root => self.path_end <= 1,
ItemPathAnchor::Crate => self.path_end == 0,
}
}
fn parent(mut self) -> Option<ItemPath> {
match self.is_toplevel() {
true => None,
false => {
self.path_end -= 1;
Some(self)
}
}
}
fn name(&self) -> Option<&str> {
self.path_end.checked_sub(1).and_then(|i| self.path_shared.get(i)).map(String::as_str)
}
fn join(mut self, s: &impl ToString) -> ItemPath {
let path = Rc::make_mut(&mut self.path_shared);
path.truncate(self.path_end);
path.push(s.to_string());
self.path_end += 1;
self
}
fn all_ancestors(&self) -> impl Iterator<Item = ItemPath> {
let first_ancestor = self.clone().parent();
std::iter::successors(first_ancestor, |ancestor| ancestor.clone().parent())
}
}
impl PartialEq for ItemPath {
fn eq(&self, other: &Self) -> bool {
self.anchor == other.anchor && self.path_components().eq(other.path_components())
}
}
impl Eq for ItemPath {}
impl Hash for ItemPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.anchor.hash(state);
self.path_components().for_each(|c| c.hash(state));
}
}
impl fmt::Display for ItemPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self.anchor {
ItemPathAnchor::Root => (),
ItemPathAnchor::Crate => f.write_str("crate")?,
}
for s in self.path_components() {
f.write_str("::")?;
f.write_str(s)?;
}
Ok(())
}
}
impl fmt::Debug for ItemPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt::Display::fmt(self, f)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ImplSymbolType {
Method,
Const,
Type,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SymbolType {
Crate,
Struct,
Trait,
Enum,
Union,
Type,
Mod,
Macro,
Const,
Fn,
Static,
ImplItem(ImplSymbolType),
}
impl SymbolType {
fn get_module_path(self, path: &ItemPath) -> Option<ItemPath> {
match self {
SymbolType::Crate => {
assert!(path.is_toplevel(), "a crate should always be in a toplevel path");
None
}
SymbolType::Struct
| SymbolType::Trait
| SymbolType::Enum
| SymbolType::Union
| SymbolType::Type
| SymbolType::Mod
| SymbolType::Macro
| SymbolType::Const
| SymbolType::Fn
| SymbolType::Static => {
let p = path.clone().parent().unwrap_or_else(|| {
panic!("item {path} of type {self:?} should have a parent module")
});
Some(p)
}
SymbolType::ImplItem(_) => {
let p = path
.clone()
.parent()
.unwrap_or_else(|| {
panic!("item {path} of type {self:?} should have a parent type")
})
.parent()
.unwrap_or_else(|| {
panic!("item {path} of type {self:?} should have a parent module")
});
Some(p)
}
}
}
}
fn symbols_type_impl_block(
module: &ItemPath,
impl_block: &syn::ItemImpl,
) -> Vec<(ItemPath, SymbolType)> {
use syn::{ImplItem, Type, TypePath};
if let Type::Path(TypePath { qself: None, path }) = &*impl_block.self_ty {
if let Some(self_ident) = path.get_ident().map(ToString::to_string) {
let self_path = module.clone().join(&self_ident);
return impl_block
.items
.iter()
.filter_map(|item| match item {
ImplItem::Fn(m) => {
let ident = m.sig.ident.to_string();
Some((ident, ImplSymbolType::Method))
}
ImplItem::Const(c) => {
let ident = c.ident.to_string();
Some((ident, ImplSymbolType::Const))
}
ImplItem::Type(t) => {
let ident = t.ident.to_string();
Some((ident, ImplSymbolType::Type))
}
_ => None,
})
.map(|(ident, tpy)| (self_path.clone().join(&ident), SymbolType::ImplItem(tpy)))
.collect();
}
}
Vec::new()
}
fn item_symbols_type(module: &ItemPath, item: &Item) -> Vec<(ItemPath, SymbolType)> {
let item_path = |ident: &syn::Ident| module.clone().join(ident);
let (path, symbol_type) = match item {
Item::Enum(e) => (item_path(&e.ident), SymbolType::Enum),
Item::Struct(s) => (item_path(&s.ident), SymbolType::Struct),
Item::Trait(t) => (item_path(&t.ident), SymbolType::Trait),
Item::Union(u) => (item_path(&u.ident), SymbolType::Union),
Item::Type(t) => (item_path(&t.ident), SymbolType::Type),
Item::Mod(m) => (item_path(&m.ident), SymbolType::Mod),
Item::Macro(syn::ItemMacro { ident: Some(ident), .. }) => {
(item_path(ident), SymbolType::Macro)
}
Item::Const(c) => (item_path(&c.ident), SymbolType::Const),
Item::Fn(f) => (item_path(&f.sig.ident), SymbolType::Fn),
Item::Static(s) => (item_path(&s.ident), SymbolType::Static),
Item::Impl(impl_block) => {
return symbols_type_impl_block(module, impl_block);
}
_ => return Vec::new(),
};
vec![(path, symbol_type)]
}
fn is_cfg_test(attribute: &syn::Attribute) -> bool {
let test_attribute: syn::Attribute = syn::parse_quote!(#[cfg(test)]);
*attribute == test_attribute
}
fn visit_module_item(
save_symbol: impl Fn(&ItemPath) -> bool,
symbols_type: &mut HashMap<ItemPath, SymbolType>,
module: &ItemPath,
item: &Item,
) {
for (symbol, symbol_type) in item_symbols_type(module, item) {
if save_symbol(&symbol) {
symbols_type.insert(symbol, symbol_type);
}
}
}
fn check_explore_module(
should_explore_module: impl Fn(&ItemPath) -> bool,
modules_visited: &mut HashSet<ItemPath>,
mod_symbol: &ItemPath,
mod_item: &ItemMod,
) -> bool {
if modules_visited.contains(mod_symbol) {
return false;
}
if mod_item.attrs.iter().any(is_cfg_test) {
return false;
}
let explore = should_explore_module(mod_symbol);
if explore {
modules_visited.insert(mod_symbol.clone());
}
explore
}
fn explore_crate<P: AsRef<Path>>(
file: P,
crate_symbol: &ItemPath,
symbols: &HashSet<ItemPath>,
paths_to_explore: &HashSet<ItemPath>,
symbols_type: &mut HashMap<ItemPath, SymbolType>,
emit_warning: &impl Fn(&str),
) -> Result<(), module_walker::ModuleWalkError> {
let mut modules_visited: HashSet<ItemPath> = HashSet::new();
symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
let mut visit = |module: &ItemPath, item: &Item| {
let save_symbol = |symbol: &ItemPath| {
symbols.contains(symbol) || paths_to_explore.contains(symbol)
};
visit_module_item(save_symbol, symbols_type, module, item);
};
let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
check_explore_module(
|mod_symbol| paths_to_explore.contains(mod_symbol),
&mut modules_visited,
mod_symbol,
mod_item,
)
};
walk_module_file(file, crate_symbol, &mut visit, &mut explore_module, emit_warning)
}
fn load_symbols_type<P: AsRef<Path>>(
entry_point: P,
symbols: &HashSet<ItemPath>,
emit_warning: &impl Fn(&str),
) -> Result<HashMap<ItemPath, SymbolType>, IntralinkError> {
let paths_to_explore: HashSet<ItemPath> = all_ancestor_paths(symbols.iter());
let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
let std_lib_crates = match references_standard_library(symbols) {
true => get_standard_libraries()?,
false => Vec::new(),
};
for Crate { name, entrypoint } in std_lib_crates {
explore_crate(
entrypoint,
&ItemPath::root(&name),
symbols,
&paths_to_explore,
&mut symbols_type,
emit_warning,
)?;
}
explore_crate(
entry_point,
&ItemPath::new(ItemPathAnchor::Crate),
symbols,
&paths_to_explore,
&mut symbols_type,
emit_warning,
)?;
Ok(symbols_type)
}
fn all_ancestor_paths<'a>(symbols: impl Iterator<Item = &'a ItemPath>) -> HashSet<ItemPath> {
symbols.into_iter().flat_map(ItemPath::all_ancestors).collect()
}
fn extract_markdown_intralink_symbols(doc: &Doc) -> HashSet<ItemPath> {
let item_paths_inline_links =
markdown_link_iterator(&doc.markdown).items().filter_map(|l| match l {
MarkdownLink::Inline { link: inline_link } => inline_link.link.link_as_item_path(),
MarkdownLink::Reference { .. } => None,
});
let item_paths_reference_link_def = markdown_reference_link_definition_iterator(&doc.markdown)
.items()
.filter_map(|l| l.link.link_as_item_path());
item_paths_inline_links.chain(item_paths_reference_link_def).collect()
}
fn documentation_url(
item_path: &ItemPath,
symbols_type: &HashMap<ItemPath, SymbolType>,
crate_name: &str,
fragment: Option<&str>,
config: &IntralinksDocsRsConfig,
) -> Option<String> {
let package_name = crate_name.replace('-', "_");
let typ = *symbols_type.get(item_path)?;
let mut link = match item_path.anchor {
ItemPathAnchor::Root => {
let std_crate_name =
item_path.path_components().next().expect("a root path should not be empty");
format!("https://doc.rust-lang.org/stable/{std_crate_name}/")
}
ItemPathAnchor::Crate => {
let base_url =
config.docs_rs_base_url.as_ref().map_or("https://docs.rs", String::as_str);
let version = config.docs_rs_version.as_ref().map_or("latest", String::as_str);
format!("{base_url}/{crate_name}/{version}/{package_name}/")
}
};
if typ == SymbolType::Crate {
return Some(format!("{}{}", link, fragment.unwrap_or("")));
}
let skip_components = match item_path.anchor {
ItemPathAnchor::Root => 1,
ItemPathAnchor::Crate => 0,
};
let module_path = typ.get_module_path(item_path).expect("item should belong to a module");
for s in module_path.path_components().skip(skip_components) {
link.push_str(s);
link.push('/');
}
let name =
item_path.name().unwrap_or_else(|| panic!("failed to get last component of {item_path}"));
match typ {
SymbolType::Crate => unreachable!(),
SymbolType::Struct => link.push_str(&format!("struct.{name}.html")),
SymbolType::Trait => link.push_str(&format!("trait.{name}.html")),
SymbolType::Enum => link.push_str(&format!("enum.{name}.html")),
SymbolType::Union => link.push_str(&format!("union.{name}.html")),
SymbolType::Type => link.push_str(&format!("type.{name}.html")),
SymbolType::Mod => link.push_str(&format!("{name}/")),
SymbolType::Macro => link.push_str(&format!("macro.{name}.html")),
SymbolType::Const => link.push_str(&format!("const.{name}.html")),
SymbolType::Fn => link.push_str(&format!("fn.{name}.html")),
SymbolType::Static => link.push_str(&format!("static.{name}.html")),
SymbolType::ImplItem(typ) => {
let parent_path = item_path
.clone()
.parent()
.unwrap_or_else(|| panic!("item {item_path} should always have a parent"));
let link = documentation_url(
&parent_path,
symbols_type,
crate_name,
None,
config,
)?;
let impl_item_fragment_str = match typ {
ImplSymbolType::Method => "method",
ImplSymbolType::Const => "associatedconstant",
ImplSymbolType::Type => "associatedtype",
};
return Some(format!("{link}#{impl_item_fragment_str}.{name}"));
}
}
Some(format!("{}{}", link, fragment.unwrap_or("")))
}
enum MarkdownLinkAction {
Link(Link),
Preserve,
Strip,
}
fn markdown_link(
link: &Link,
symbols_type: &HashMap<ItemPath, SymbolType>,
crate_name: &str,
emit_warning: &impl Fn(&str),
config: &IntralinksConfig,
) -> MarkdownLinkAction {
match link.link_as_item_path() {
Some(symbol) => {
let link = documentation_url(
&symbol,
symbols_type,
crate_name,
link.link_fragment(),
&config.docs_rs,
);
match link {
Some(l) => MarkdownLinkAction::Link(l.into()),
None => {
emit_warning(&format!("Could not resolve definition of `{symbol}`."));
MarkdownLinkAction::Strip
}
}
}
None => MarkdownLinkAction::Preserve,
}
}
fn rewrite_markdown_links(
doc: &Doc,
symbols_type: &HashMap<ItemPath, SymbolType>,
crate_name: &str,
emit_warning: &impl Fn(&str),
config: &IntralinksConfig,
reference_links_to_remove: &HashSet<UniCase<String>>,
) -> Doc {
use crate::utils::ItemOrOther;
let strip_links = config.strip_links.unwrap_or(false);
let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
for item_or_other in markdown_link_iterator(&doc.markdown).complete() {
match item_or_other {
ItemOrOther::Item(MarkdownLink::Inline { link: inline_link }) => {
let markdown_link: MarkdownLinkAction = match strip_links {
false => markdown_link(
&inline_link.link,
symbols_type,
crate_name,
emit_warning,
config,
),
true => match inline_link.link.link_as_item_path() {
None => MarkdownLinkAction::Preserve,
Some(_) => MarkdownLinkAction::Strip,
},
};
match markdown_link {
MarkdownLinkAction::Link(markdown_link) => {
new_doc.push_str(&inline_link.with_link(markdown_link).to_string());
}
MarkdownLinkAction::Preserve => {
new_doc.push_str(&inline_link.to_string());
}
MarkdownLinkAction::Strip => {
new_doc.push_str(&inline_link.text);
}
}
}
ItemOrOther::Item(MarkdownLink::Reference { link }) => {
match reference_links_to_remove.contains(link.label()) {
true => new_doc.push_str(link.text()),
false => new_doc.push_str(&link.to_string()),
}
}
ItemOrOther::Other(other) => {
new_doc.push_str(other);
}
}
}
Doc::from_str(new_doc)
}
struct RewriteReferenceLinksResult {
doc: Doc,
reference_links_to_remove: HashSet<UniCase<String>>,
}
fn rewrite_reference_links_definitions(
doc: &Doc,
symbols_type: &HashMap<ItemPath, SymbolType>,
crate_name: &str,
emit_warning: &impl Fn(&str),
config: &IntralinksConfig,
) -> RewriteReferenceLinksResult {
use crate::utils::ItemOrOther;
let mut reference_links_to_remove = HashSet::new();
let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
let mut skip_next_newline = false;
let strip_links = config.strip_links.unwrap_or(false);
let iter = markdown_reference_link_definition_iterator(&doc.markdown);
for item_or_other in iter.complete() {
match item_or_other {
ItemOrOther::Item(link_ref_def) => {
let markdown_link: MarkdownLinkAction = match strip_links {
false => markdown_link(
&link_ref_def.link,
symbols_type,
crate_name,
emit_warning,
config,
),
true => match link_ref_def.link.link_as_item_path() {
None => MarkdownLinkAction::Preserve,
Some(_) => MarkdownLinkAction::Strip,
},
};
match markdown_link {
MarkdownLinkAction::Link(link) => {
new_doc.push_str(&link_ref_def.with_link(link).to_string());
}
MarkdownLinkAction::Preserve => {
new_doc.push_str(&link_ref_def.to_string());
}
MarkdownLinkAction::Strip => {
reference_links_to_remove.insert(link_ref_def.label);
skip_next_newline = true;
}
}
}
ItemOrOther::Other(other) => {
let other = match skip_next_newline {
true => {
skip_next_newline = false;
let next_index = other
.chars()
.enumerate()
.skip_while(|(_, c)| c.is_whitespace() && *c != '\n')
.skip(1)
.map(|(i, _)| i)
.next();
next_index.and_then(|i| other.get(i..)).unwrap_or("")
}
false => other,
};
new_doc.push_str(other);
}
}
}
RewriteReferenceLinksResult { doc: Doc::from_str(new_doc), reference_links_to_remove }
}
fn get_rustc_sysroot_libraries_dir() -> Result<PathBuf, IntralinkError> {
use std::process::Command;
let output = Command::new("rustc")
.args(["--print=sysroot"])
.output()
.map_err(|e| IntralinkError::LoadStdLibError(format!("failed to run rustc: {e}")))?;
let s = String::from_utf8(output.stdout).expect("unexpected output from rustc");
let sysroot = PathBuf::from(s.trim());
let src_path = sysroot.join("lib").join("rustlib").join("src").join("rust").join("library");
match src_path.is_dir() {
false => Err(IntralinkError::LoadStdLibError(format!(
"Cannot find rust standard library in \"{}\"",
src_path.display()
))),
true => Ok(src_path),
}
}
#[derive(Debug)]
struct Crate {
name: String,
entrypoint: PathBuf,
}
fn references_standard_library(symbols: &HashSet<ItemPath>) -> bool {
symbols.iter().any(|symbol| symbol.anchor == ItemPathAnchor::Root)
}
fn get_standard_libraries() -> Result<Vec<Crate>, IntralinkError> {
let libraries_dir = get_rustc_sysroot_libraries_dir()?;
let mut std_libs = Vec::with_capacity(64);
for entry in std::fs::read_dir(libraries_dir)? {
let entry = entry?;
let project_dir_path = entry.path();
let cargo_manifest_path = project_dir_path.join("Cargo.toml");
let lib_entrypoint = project_dir_path.join("src").join("lib.rs");
if cargo_manifest_path.is_file() && lib_entrypoint.is_file() {
let crate_name =
crate::project_package_name(&cargo_manifest_path).ok_or_else(|| {
IntralinkError::LoadStdLibError(format!(
"failed to load manifest in \"{}\"",
cargo_manifest_path.display()
))
})?;
let crate_info = Crate { name: crate_name, entrypoint: lib_entrypoint };
std_libs.push(crate_info);
}
}
Ok(std_libs)
}
#[allow(clippy::too_many_lines)]
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use module_walker::walk_module_items;
use std::cell::RefCell;
fn item_path(id: &str) -> ItemPath {
ItemPath::from_string(id).unwrap()
}
#[test]
fn test_item_path_is_toplevel() {
assert!(!item_path("crate::baz::mumble").is_toplevel());
assert!(!item_path("::std::baz::mumble").is_toplevel());
assert!(!item_path("crate::baz").is_toplevel());
assert!(!item_path("::std::baz").is_toplevel());
assert!(item_path("crate").is_toplevel());
assert!(item_path("::std").is_toplevel());
}
#[test]
fn test_item_path_parent() {
assert_eq!(item_path("crate::baz::mumble").parent(), Some(item_path("crate::baz")));
assert_eq!(item_path("::std::baz::mumble").parent(), Some(item_path("::std::baz")));
assert_eq!(item_path("crate::baz").parent(), Some(item_path("crate")));
assert_eq!(item_path("::std::baz").parent(), Some(item_path("::std")));
assert_eq!(item_path("crate").parent(), None);
assert_eq!(item_path("::std").parent(), None);
}
#[test]
fn test_item_path_join() {
assert_eq!(item_path("crate::foo").join(&"bar"), item_path("crate::foo::bar"),);
assert_eq!(item_path("::std::foo").join(&"bar"), item_path("::std::foo::bar"),);
assert_eq!(
item_path("::std::foo::bar").parent().unwrap().join(&"baz"),
item_path("::std::foo::baz"),
);
}
#[test]
fn test_all_ancestor_paths() {
let symbols = [
item_path("crate::foo::bar::baz"),
item_path("crate::baz::mumble"),
item_path("::std::vec::Vec"),
];
let expected: HashSet<ItemPath> = [
item_path("crate"),
item_path("crate::foo"),
item_path("crate::foo::bar"),
item_path("crate::baz"),
item_path("::std"),
item_path("::std::vec"),
]
.into_iter()
.collect();
assert_eq!(all_ancestor_paths(symbols.iter()), expected);
}
fn explore_crate(
ast: &[Item],
dir: &Path,
crate_symbol: &ItemPath,
should_explore_module: impl Fn(&ItemPath) -> bool,
symbols_type: &mut HashMap<ItemPath, SymbolType>,
emit_warning: impl Fn(&str),
) {
let mut modules_visited: HashSet<ItemPath> = HashSet::new();
symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
let mut visit = |module: &ItemPath, item: &Item| {
visit_module_item(|_| true, symbols_type, module, item);
};
let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
check_explore_module(&should_explore_module, &mut modules_visited, mod_symbol, mod_item)
};
walk_module_items(ast, dir, crate_symbol, &mut visit, &mut explore_module, &emit_warning)
.ok()
.unwrap();
}
#[test]
fn test_walk_module_and_symbols_type() {
let module_skip: ItemPath = item_path("crate::skip");
let source = indoc! { "
struct AStruct {}
mod skip {
struct Skip {}
}
mod a {
mod b {
trait ATrait {}
}
struct FooStruct {}
}
"
};
let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
let warnings = RefCell::new(Vec::new());
explore_crate(
&syn::parse_file(source).unwrap().items,
&PathBuf::new(),
&item_path("crate"),
|m| *m != module_skip,
&mut symbols_type,
|msg| warnings.borrow_mut().push(msg.to_owned()),
);
let expected: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::AStruct"), SymbolType::Struct),
(item_path("crate::skip"), SymbolType::Mod),
(item_path("crate::a"), SymbolType::Mod),
(item_path("crate::a::b"), SymbolType::Mod),
(item_path("crate::a::b::ATrait"), SymbolType::Trait),
(item_path("crate::a::FooStruct"), SymbolType::Struct),
]
.into_iter()
.collect();
assert_eq!(symbols_type, expected);
}
#[test]
fn test_symbols_type_with_mod_under_cfg_test() {
let source = indoc! { "
#[cfg(not(test))]
mod a {
struct MyStruct {}
}
#[cfg(test)]
mod a {
struct MyStructTest {}
}
#[cfg(test)]
mod b {
struct MyStructTest {}
}
#[cfg(not(test))]
mod b {
struct MyStruct {}
}
"
};
let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
let warnings = RefCell::new(Vec::new());
explore_crate(
&syn::parse_file(source).unwrap().items,
&PathBuf::new(),
&item_path("crate"),
|_| true,
&mut symbols_type,
|msg| warnings.borrow_mut().push(msg.to_owned()),
);
let expected: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::a"), SymbolType::Mod),
(item_path("crate::a::MyStruct"), SymbolType::Struct),
(item_path("crate::b"), SymbolType::Mod),
(item_path("crate::b::MyStruct"), SymbolType::Struct),
]
.into_iter()
.collect();
assert_eq!(symbols_type, expected);
}
#[test]
fn test_symbols_type_multiple_module_first_wins() {
let source = indoc! { "
#[cfg(not(foo))]
mod a {
struct MyStruct {}
}
#[cfg(foo)]
mod a {
struct Skip {}
}
"
};
let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
let warnings = RefCell::new(Vec::new());
explore_crate(
&syn::parse_file(source).unwrap().items,
&PathBuf::new(),
&item_path("crate"),
|_| true,
&mut symbols_type,
|msg| warnings.borrow_mut().push(msg.to_owned()),
);
let expected: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::a"), SymbolType::Mod),
(item_path("crate::a::MyStruct"), SymbolType::Struct),
]
.into_iter()
.collect();
assert_eq!(symbols_type, expected);
}
#[test]
fn test_traverse_module_expore_lazily() {
let symbols: HashSet<ItemPath> = [item_path("crate::module")].into_iter().collect();
let modules = all_ancestor_paths(symbols.iter());
let source = indoc! { "
mod module {
struct Foo {}
}
"
};
let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
let warnings = RefCell::new(Vec::new());
explore_crate(
&syn::parse_file(source).unwrap().items,
&PathBuf::new(),
&item_path("crate"),
|module| modules.contains(module),
&mut symbols_type,
|msg| warnings.borrow_mut().push(msg.to_owned()),
);
let symbols_type: HashSet<ItemPath> = symbols_type.keys().cloned().collect();
let expected: HashSet<ItemPath> =
[item_path("crate"), item_path("crate::module")].into_iter().collect();
assert_eq!(symbols_type, expected);
}
#[test]
fn test_documentation_url() {
let config = IntralinksDocsRsConfig::default();
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate"), SymbolType::Crate)].into_iter().collect();
let link = documentation_url(&item_path("crate"), &symbols_type, "foobini", None, &config);
assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/"));
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::AStruct"), SymbolType::Struct)].into_iter().collect();
let link = documentation_url(
&item_path("crate::AStruct"),
&symbols_type,
"foobini",
None,
&config,
);
assert_eq!(
link.as_deref(),
Some("https://docs.rs/foobini/latest/foobini/struct.AStruct.html")
);
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
let link = documentation_url(
&item_path("crate::amodule"),
&symbols_type,
"foobini",
None,
&config,
);
assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/amodule/"));
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("::std"), SymbolType::Crate)].into_iter().collect();
let link = documentation_url(&item_path("::std"), &symbols_type, "foobini", None, &config);
assert_eq!(link.as_deref(), Some("https://doc.rust-lang.org/stable/std/"));
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("::std::collections::HashMap"), SymbolType::Struct)].into_iter().collect();
let link = documentation_url(
&item_path("::std::collections::HashMap"),
&symbols_type,
"foobini",
None,
&config,
);
assert_eq!(
link.as_deref(),
Some("https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html")
);
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
let link = documentation_url(
&ItemPath::from_string("crate::amodule").unwrap(),
&symbols_type,
"foo-bar-mumble",
None,
&config,
);
assert_eq!(
link.as_deref(),
Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amodule/")
);
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate"), SymbolType::Crate)].into_iter().collect();
let link = documentation_url(
&ItemPath::from_string("crate").unwrap(),
&symbols_type,
"foo-bar-mumble",
Some("#enums"),
&config,
);
assert_eq!(
link.as_deref(),
Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/#enums")
);
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::amod"), SymbolType::Mod)].into_iter().collect();
let link = documentation_url(
&ItemPath::from_string("crate::amod").unwrap(),
&symbols_type,
"foo-bar-mumble",
Some("#structs"),
&config,
);
assert_eq!(
link.as_deref(),
Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amod/#structs")
);
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::MyStruct"), SymbolType::Struct)].into_iter().collect();
let link = documentation_url(
&ItemPath::from_string("crate::MyStruct").unwrap(),
&symbols_type,
"foo-bar-mumble",
Some("#implementations"),
&config,
);
assert_eq!(
link.as_deref(),
Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/struct.MyStruct.html#implementations")
);
let symbols_type: HashMap<ItemPath, SymbolType> = [
(item_path("crate::mymod::MyStruct"), SymbolType::Struct),
(
item_path("crate::mymod::MyStruct::a_method"),
SymbolType::ImplItem(ImplSymbolType::Method),
),
]
.into_iter()
.collect();
let link = documentation_url(
&ItemPath::from_string("crate::mymod::MyStruct::a_method").unwrap(),
&symbols_type,
"foo-bar-mumble",
Some("#thiswillbedropped"),
&config,
);
assert_eq!(
link.as_deref(),
Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/mymod/struct.MyStruct.html#method.a_method")
);
let config = IntralinksDocsRsConfig {
docs_rs_base_url: Some("https://docs.company.rs".to_owned()),
docs_rs_version: Some("1.0.0".to_owned()),
};
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::Foo"), SymbolType::Struct)].into_iter().collect();
let link =
documentation_url(&item_path("crate::Foo"), &symbols_type, "foobini", None, &config);
assert_eq!(
link.as_deref(),
Some("https://docs.company.rs/foobini/1.0.0/foobini/struct.Foo.html")
);
}
#[test]
fn test_extract_markdown_intralink_symbols() {
let doc = indoc! { "
# Foobini
This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
Go ahead and check all the [structs in foo](crate::foo#structs).
Also check [this](::std::sync::Arc) and [this](::alloc::sync::Arc).
We also support [reference][style] [links].
[style]: crate::amodule
[links]: crate::foo#structs
"
};
let symbols = extract_markdown_intralink_symbols(&Doc::from_str(doc));
let expected: HashSet<ItemPath> = [
item_path("crate"),
item_path("crate::amodule"),
item_path("crate::foo"),
item_path("::std::sync::Arc"),
item_path("::alloc::sync::Arc"),
]
.into_iter()
.collect();
assert_eq!(symbols, expected);
}
#[test]
fn test_rewrite_markdown_links() {
let doc = indoc! { r"
# Foobini
This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
should [wor\\k \[fi\]ne](f\\i\(n\)e).
Go ahead and check all the [structs in foo](crate::foo#structs) specifically
[this one](crate::foo::BestStruct). Also, this is a nice function: [copy](::std::fs::copy).
[](crate::foo::BestStruct)
"
};
let symbols_type: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::amodule"), SymbolType::Mod),
(item_path("crate::foo"), SymbolType::Mod),
(item_path("crate::foo::BestStruct"), SymbolType::Struct),
(item_path("::std::fs::copy"), SymbolType::Fn),
]
.into_iter()
.collect();
let new_readme = rewrite_markdown_links(
&Doc::from_str(doc),
&symbols_type,
"foobini",
&|_| (),
&IntralinksConfig::default(),
&HashSet::new(),
);
let expected = indoc! { r"
# Foobini
This [beautiful crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/)
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
This link is broken and this is not supported, but this
should [wor\\k \[fi\]ne](f\\i\(n\)e).
Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically
[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).
[](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
"
};
assert_eq!(new_readme.as_string(), expected);
}
#[test]
fn test_rewrite_markdown_links_strip_links() {
let doc = indoc! { r"
# Foobini
This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
should [wor\\k \[fi\]ne](f\\i\(n\)e).
Go ahead and check all the [structs in foo](crate::foo#structs) specifically
[this one](crate::foo::BestStruct). Also, this is a nice function: [copy](::std::fs::copy).
[](crate::foo::BestStruct)
It works with backtricks as well: [modules](`crate::amodule`). And with
[reference-style links][ref] (preserving other [references][other]).
[ref]: crate::foo::AnotherStruct
[other]: https://en.wikipedia.org/wiki/Reference_(computer_science)
"
};
let symbols_type: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::amodule"), SymbolType::Mod),
(item_path("crate::foo"), SymbolType::Mod),
(item_path("crate::foo::BestStruct"), SymbolType::Struct),
(item_path("crate::foo::AnotherStruct"), SymbolType::Struct),
]
.into_iter()
.collect();
let new_readme = rewrite_links(
&Doc::from_str(doc),
&symbols_type,
"foobini",
&|_| (),
&IntralinksConfig { strip_links: Some(true), ..Default::default() },
);
let expected = indoc! { r"
# Foobini
This beautiful crate is cool because it contains modules
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
This link is broken and this is not supported, but this
should [wor\\k \[fi\]ne](f\\i\(n\)e).
Go ahead and check all the structs in foo specifically
this one. Also, this is a nice function: copy.

It works with backtricks as well: modules. And with
reference-style links (preserving other [references][other]).
[other]: https://en.wikipedia.org/wiki/Reference_(computer_science)
"
};
assert_eq!(new_readme.as_string(), expected);
}
#[test]
fn test_rewrite_markdown_links_backticked() {
let doc = indoc! { r"
# Foobini
This [beautiful crate](`crate`) is cool because it contains [modules](`crate::amodule`)
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
This link is [broken](`crate::broken`) and this is [not supported](`::foo::bar`), but this
should [wor\\k \[fi\]ne](f\\i\(n\)e).
Go ahead and check all the [structs in foo](`crate::foo#structs`) and
[structs in foo](`crate::foo`#structs) specifically [this one](`crate::foo::BestStruct`).
Also, this is a nice function: [copy](`::std::fs::copy`).
[](`crate::foo::BestStruct`)
"
};
let symbols_type: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::amodule"), SymbolType::Mod),
(item_path("crate::foo"), SymbolType::Mod),
(item_path("crate::foo::BestStruct"), SymbolType::Struct),
(item_path("::std::fs::copy"), SymbolType::Fn),
]
.into_iter()
.collect();
let new_readme = rewrite_markdown_links(
&Doc::from_str(doc),
&symbols_type,
"foobini",
&|_| (),
&IntralinksConfig::default(),
&HashSet::new(),
);
let expected = indoc! { r"
# Foobini
This [beautiful crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/)
and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
This link is broken and this is not supported, but this
should [wor\\k \[fi\]ne](f\\i\(n\)e).
Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) and
[structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically [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).
[](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
"
};
assert_eq!(new_readme.as_string(), expected);
}
#[test]
fn test_markdown_reference_definitions() {
let doc = indoc! { r#"
# Foobini
This [beautiful crate] is cool because it contains [modules]
and some other [stuff] as well.
This link is [broken] and this is [not supported],
but this should [wor\\k \[fi\]ne].
Go ahead and check all the [structs in foo] specifically
[this one]. Also, this is a nice function: [copy][cp].
[![BestStruct doc]][BestStruct]
[beautiful crate]: crate
[modules]: crate::amodule
[stuff]: https://en.wikipedia.org/wiki/Stuff
[broken]: crate::broken
[not supported]: ::foo::bar
[wor\\k \[fi\]ne]: f\\i\(n\)e
[structs in foo]: crate::foo#structs
[this one]: crate::foo::BestStruct
[cp]: ::std::fs::copy#examples "A title here"
[BestStruct doc]: https://example.com/image.png
[BestStruct]: crate::foo::BestStruct
"#
};
let symbols_type: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::amodule"), SymbolType::Mod),
(item_path("crate::foo"), SymbolType::Mod),
(item_path("crate::foo::BestStruct"), SymbolType::Struct),
(item_path("::std::fs::copy"), SymbolType::Fn),
]
.into_iter()
.collect();
let new_readme = rewrite_links(
&Doc::from_str(doc),
&symbols_type,
"foobini",
&|_| (),
&IntralinksConfig::default(),
);
let expected = indoc! { r#"
# Foobini
This [beautiful crate] is cool because it contains [modules]
and some other [stuff] as well.
This link is broken and this is not supported,
but this should [wor\\k \[fi\]ne].
Go ahead and check all the [structs in foo] specifically
[this one]. Also, this is a nice function: [copy][cp].
[![BestStruct doc]][BestStruct]
[beautiful crate]: https://docs.rs/foobini/latest/foobini/
[modules]: https://docs.rs/foobini/latest/foobini/amodule/
[stuff]: https://en.wikipedia.org/wiki/Stuff
[wor\\k \[fi\]ne]: f\\i\(n\)e
[structs in foo]: https://docs.rs/foobini/latest/foobini/foo/#structs
[this one]: https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html
[cp]: https://doc.rust-lang.org/stable/std/fs/fn.copy.html#examples "A title here"
[BestStruct doc]: https://example.com/image.png
[BestStruct]: https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html
"#
};
assert_eq!(new_readme.as_string(), expected);
}
#[test]
fn test_rewrite_markdown_links_removes_links() {
let doc = indoc! { r"
# Foobini
This crate has multiple [modules][mod a]. This link is [broken] and [so is this][null].
[mod a]: crate::amodule
[broken]: crate::broken
[null]: crate::nothing
"
};
let symbols_type: HashMap<ItemPath, SymbolType> =
[(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
let new_readme = rewrite_links(
&Doc::from_str(doc),
&symbols_type,
"foobini",
&|_| (),
&IntralinksConfig::default(),
);
let expected = indoc! { r"
# Foobini
This crate has multiple [modules][mod a]. This link is broken and so is this.
[mod a]: https://docs.rs/foobini/latest/foobini/amodule/
"
};
assert_eq!(new_readme.as_string(), expected);
}
#[test]
fn test_markdown_impl_items() {
let doc = indoc! { r#"
# Foobini
This crate has [`Foo::new()`](`crate::Foo::new`), [`Foo::a_method()`](`crate::Foo::a_method`),
and [`Foo::another_method()`](`crate::Foo::another_method`).
It also has [`Foo::no_self()`](`crate::Foo::no_self`). There's also [`Bar::beer()`](`crate::amod::Bar::beer`).
Struct `Foo` has a [type called `baz`](`crate::Foo::Baz`) and a
[const called `number`](`crate::Foo::number`).
We have a function in `FooAlias` [called `hello`](`crate::FooAlias::hello`).
And in `MyEnum` we have [called `hey`](`crate::MyEnum::hey`).
And in `MyUnion` we have [called `sup`](`crate::MyUnion::sup`).
"#
};
let symbols_type: HashMap<ItemPath, SymbolType> = [
(item_path("crate"), SymbolType::Crate),
(item_path("crate::Foo"), SymbolType::Struct),
(item_path("crate::Foo::new"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::Foo::a_method"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::Foo::another_method"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::Foo::no_self"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::amod::Bar"), SymbolType::Struct),
(item_path("crate::amod::Bar::beer"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::Foo::Baz"), SymbolType::ImplItem(ImplSymbolType::Type)),
(item_path("crate::Foo::number"), SymbolType::ImplItem(ImplSymbolType::Const)),
(item_path("crate::FooAlias"), SymbolType::Type),
(item_path("crate::FooAlias::hello"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::MyEnum"), SymbolType::Enum),
(item_path("crate::MyEnum::hey"), SymbolType::ImplItem(ImplSymbolType::Method)),
(item_path("crate::MyUnion"), SymbolType::Union),
(item_path("crate::MyUnion::sup"), SymbolType::ImplItem(ImplSymbolType::Method)),
]
.into_iter()
.collect();
let new_readme = rewrite_links(
&Doc::from_str(doc),
&symbols_type,
"foobini",
&|_| (),
&IntralinksConfig::default(),
);
let expected = indoc! { r#"
# Foobini
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),
and [`Foo::another_method()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.another_method).
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).
Struct `Foo` has a [type called `baz`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#associatedtype.Baz) and a
[const called `number`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#associatedconstant.number).
We have a function in `FooAlias` [called `hello`](https://docs.rs/foobini/latest/foobini/type.FooAlias.html#method.hello).
And in `MyEnum` we have [called `hey`](https://docs.rs/foobini/latest/foobini/enum.MyEnum.html#method.hey).
And in `MyUnion` we have [called `sup`](https://docs.rs/foobini/latest/foobini/union.MyUnion.html#method.sup).
"#
};
assert_eq!(new_readme.as_string(), expected);
}
}