use std::path::{Path, PathBuf};
use syn::Ident;
use syn::Item;
use thiserror::Error;
use super::ItemPath;
#[derive(Error, Debug)]
pub enum ModuleWalkError {
#[error("IO error: {0}")]
IOError(std::io::Error),
#[error("failed to parse rust file: {0}")]
ParseError(syn::Error),
}
impl From<std::io::Error> for ModuleWalkError {
fn from(err: std::io::Error) -> Self {
ModuleWalkError::IOError(err)
}
}
impl From<syn::Error> for ModuleWalkError {
fn from(err: syn::Error) -> Self {
ModuleWalkError::ParseError(err)
}
}
fn file_ast<P: AsRef<Path>>(filepath: P) -> Result<syn::File, ModuleWalkError> {
let src = std::fs::read_to_string(filepath)?;
Ok(syn::parse_file(&src)?)
}
fn module_filename(dir: &Path, module: &Ident) -> Option<PathBuf> {
let mod_file = dir.join(format!("{module}.rs"));
if mod_file.is_file() {
return Some(mod_file);
}
let mod_file = dir.join(module.to_string()).join("mod.rs");
if mod_file.is_file() {
return Some(mod_file);
}
None
}
pub(super) fn walk_module_items(
ast: &[Item],
dir: &Path,
mod_symbol: &ItemPath,
visit: &mut impl FnMut(&ItemPath, &Item),
explore_module: &mut impl FnMut(&ItemPath, &syn::ItemMod) -> bool,
emit_warning: &impl Fn(&str),
) -> Result<(), ModuleWalkError> {
for item in ast {
visit(mod_symbol, item);
if let Item::Mod(module) = item {
let child_module_symbol: ItemPath = mod_symbol.clone().join(&module.ident.to_string());
if explore_module(&child_module_symbol, module) {
match &module.content {
Some((_, items)) => {
walk_module_items(
items,
dir,
&child_module_symbol,
visit,
explore_module,
emit_warning,
)?;
}
None => match module_filename(dir, &module.ident) {
None => emit_warning(&format!(
"Unable to find module file for module {} in directory \"{}\"",
child_module_symbol,
dir.display()
)),
Some(mod_filename) => walk_module_file(
mod_filename,
&child_module_symbol,
visit,
explore_module,
emit_warning,
)?,
},
}
}
}
}
Ok(())
}
pub(super) fn walk_module_file<P: AsRef<Path>>(
file: P,
mod_symbol: &ItemPath,
visit: &mut impl FnMut(&ItemPath, &Item),
explore_module: &mut impl FnMut(&ItemPath, &syn::ItemMod) -> bool,
emit_warning: &impl Fn(&str),
) -> Result<(), ModuleWalkError> {
let dir: &Path = file
.as_ref()
.parent()
.unwrap_or_else(|| panic!("failed to get directory of \"{}\"", file.as_ref().display()));
let ast: syn::File = file_ast(&file)?;
walk_module_items(&ast.items, dir, mod_symbol, visit, explore_module, emit_warning)
}